약 4개월 전, 이전에 일하고 있던 회사에서 퇴사를 했다. 당시 회사에서는 기존에 진행했던 프로젝트를 마무리하고 새 프로젝트를 진행할 예정이었는데, 오래전부터 퇴사를 마음먹고 있었던지라 새 프로젝트 중간에 퇴사하는 것보다 깔끔하게 퇴사하는 것이 낫겠다 싶어서 다음이 정해지지 않은 상태에서 과감하게 퇴사를 결정했다. 퇴사를 결정하게 된 사유 중에는 잦은 요구사항 변경 요청이 있었다.

퇴사 전 프로젝트를 진행했던 팀에서는 테스트 코드 작성을 하지 않았었다. 아니, 할 생각조차 못했다고 말하는 것이 맞을 것이다. 팀은 고객사의 잦은 요구사항 변경, 팀에서 준비되지 않은 기술을 강제로 사용해야 하는 등 여유가 없는 상태였으니. 아무튼, 이런저런 핑계로 테스트 코드 작성은 외면받는 상황이었다. 그러한 상황에서 새로운 요구 사항을 위해 코드를 수정하면 가끔씩 이전에 동작하던 기능에서 문제가 발생하곤 했고 프로젝트가 진행될 수록 이는 더더욱 잦아졌다. 무엇보다 가장 큰 문제는 그 때 당시의 나는 테스트 코드의 존재를 몰랐었다.

 

문제의 나비효과

요구사항이 발생하면 해당 요구사항에 맞게 코드를 수정 및 보완한다. 코드를 수정하지 않고 해결할 다른 방법이 있지 않는 이상 지극히 당연한 명제이다. 다만 동시에 고려해야 할 명제가 있는데 그것은 바로 ‘이전에 동작하던 기능에는 문제가 없어야 한다’ 는 명제이며 정책이 완전히 바뀌지 않는 상황이 아니라면 두 명제는 대부분의 경우 && 이다. 문제는 코드를 수정 및 보완하는 작업은 언제나 이전에 동작하던 기능을 건드리는 위험에 항상 노출되어 있다는 것인데 위에서 언급했듯이 이는 코드가 누적될 수록 심해진다.

 

테스트 코드의 존재를 알게 되다.

테스트 코드라는 것을 처음 알게 된 것은 JPA를 공부하기 위해 인프런에 있는 김영한 님의 강의를 수강하면서부터였다. 강의에서는 테스트 코드를 Given-When-Then 의 단계로 구분하여 코드를 작성하고 여러 경우의 간단한 테스트 코드를 통해 검증하는 모습을 보여주었는데 그 중 눈에 띄는 부분은 ‘실패 테스트 코드’ 작성이었다.

 

코드의 실패를 고민해야한다.

프로그램은 여러가지 이유로 예외가 발생한다. 파라미터의 타입이 맞지 않거나 요청에 필요한 권한이 없거나 존재하지 않는 리소스를 요청하는 등 여러 경우의 수가 존재하며 개발자는 이를 최대한 대처해야 할 의무가 있다. 실패할 상황에 대해 고민하고 그에 대한 대비책을 마련해야한다. 이를 외면한다면 프로그램은 버그투성이에 방치되고 해당 프로그램을 서비스하는 회사는 폐업할 것이며 개발자는 해고 통보를 받게 될 것이다.

실패 테스트 코드를 작성한다는 것은 바꿔말하면 실패할 수 있는 상황을 고민한다는 것과 같다. 작성한 코드가 어떤 상황에서 실패할 수 있는지, 실패하는 상황을 대처할 코드는 작성되어 있는지 점검하는 기회를 제공해 주는 것이다. 돌이켜보면 테스트 코드의 존재를 몰랐던 시기의 나는 확실히 이 기회를 놓치고 있었다.

 

코드는 누적된다. 테스트 코드 역시 마찬가지다.

테스트 코드의 존재를 알게 된 이후로 개인 프로젝트를 진행하면서 테스트 코드를 작성하는 습관을 들이기 시작했다. 아직 ‘테스트 주도 개발’ 같은 책을 읽어보거나 제대로 공부한 것은 아니지만 나름대로 테스트 코드를 작성해나갔다. 테스트에 필요한 값을 설정하고, 실행하고, 실행 결과를 확인하는 코드들을 작성했다.

프로젝트가 진행될 수록 코드는 누적되었고 테스트 코드 역시 코드이기에 마찬가지였다. 오히려 테스트 코드의 양이 더 많아져 처음에는 테스트 코드가 정말 효율이 있나 싶었지만 이런 고민은 얼마 지나지 않아 더 이상 할 필요가 없음을 깨달았다. 누적된 테스크 코드는 수정 및 변경 작업 후 다시 실행시켜 봄으로써 이전의 기능들이 이상없이 동작하는지, 이상이 발생했다면 수정된 코드가 어떤 문제를 발생시키는지 점검해주는 좋은 도구가 되어주었기 때문이다.

@Test
@DisplayName("잘못된 형식의 값으로 저장")
void postPet_wrongFieldTypes() throws Exception{
    System.out.println("PostTest.postPet_wrongFieldTypes");
    // given
    PetAdd petAdd = PetAdd.builder()
            .name("dog 123")
            .weight(-10.5)
            .age(0)
            .categories(List.of("야옹이", "길냥이", "혼종"))
            .build();
    session.setAttribute(LoginAccount.value, new SessionAccount(account));

    // when & then
    mockMvc.perform(post("/pets")
                    .contentType(APPLICATION_JSON)
                    .content(mapper.writeValueAsString(petAdd))
                    .with(attributes)
                    .session(session)
            )
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST.value()))
            .andExpect(jsonPath("$.message").value(ExceptionController.BIND_EXCEPTION_MESSAGE))
            .andExpect(jsonPath("$.validation.name").value(NamePattern.MESSAGE))
            .andExpect(jsonPath("$.validation.weight").value(PositiveNumberPattern.MESSAGE))
            .andExpect(jsonPath("$.validation.categories").value(CategoryPattern.MESSAGE))
            .andExpect(jsonPath("$.validation.age").value(PositiveNumberPattern.MESSAGE));
}

@Test
@DisplayName("펫 정보 저장")
void postPet() throws Exception {
    System.out.println("PostTest.postPet");
    // given
    PetAdd petAdd = PetAdd.builder()
            .name("cat")
            .weight(10.5)
            .age(3)
            .categories(List.of(PetType.CAT.name(), CatKindCategory.NORWEGIAN.name(), Gender.FEMALE.name()))
            .build();
    session.setAttribute(LoginAccount.value, new SessionAccount(account));

    // when
    mockMvc.perform(post("/pets")
                    .contentType(APPLICATION_JSON)
                    .content(mapper.writeValueAsString(petAdd))
                    .with(attributes)
                    .session(session)
            )
            .andExpect(status().isOk());

    // then
    assertThat(petRepository.count()).isEqualTo(1);
}

개인 프로젝트에서 작성한 테스트 코드 실행 결과. 총 177개의 테스트 코드가 작성되었고 해당 테스트를 전부 통과해야 한다.

 

마치며...

최근에 백기선 님의 유튜브 영상 중 ‘더 나은 개발자로 성장하는 팁 - 나는 그런 개발자가 좋더라’ 을 보게 되었는데 그 영상에서 백기선 님은 ‘테스트 코드를 잘짜는 개발자의 코드를 신뢰한다’ 라고 말씀하셨었다. 그 말을 듣고 과거의 나에게 질문해보았다.

 

‘내가 작성한 코드는 신뢰할 수 있었나?’

 

아니요. 답은 명백했고 실제로도 그랬다. 과거의 테스트 코드를 작성하지 않아서 겪었던 경험이 실패 테스트 코드로서 명백히 말해주고 있다.

애너테이션

  • 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종
    • 메타데이터 - 데이터에 대한 데이터, 즉 데이터에 대해 설명이나 정보를 제공하기 위한 목적의 데이터
@RequiredArgsConstructor
public class PaymentRepositoryImpl implements PaymentRepositoryCustom {

    private final JPAQueryFactory queryFactory;

	// @Override는 해당 애너테이션이 붙은 메서드가 오버라이드 메서드임을 알려주는 애너테이션이다.
    @Override
    public Page<Payment> findByNameContains(String word, Pageable pageable) {
        List<Payment> payments = queryFactory.selectFrom(payment)
                .leftJoin(payment.student, student).fetchJoin()
                .where(searchCondition(word))
                .limit(pageable.getPageSize())
                .offset(pageable.getOffset())
                .orderBy(payment.id.desc())
                .fetch();

        int size = queryFactory.selectFrom(payment)
                .where(searchCondition(word))
                .fetch()
                .size();

        return PageableExecutionUtils.getPage(payments, pageable, () -> size);
    }
}

 

메타 애너테이션

  • 애너테이션을 정의하는데 사용되는 애너테이션. 애너테이션을 정의할 때에만 사용할 수 있다.

@Retention

  • 애너테이션이 유지되는 생명주기를 설정하는 메타 애너테이션
    • SOURCE
      • 소스 코드에서만 유지. 컴파일 시점에 제거됨
    • CLASS
      • 컴파일된 .class 파일에 애너테이션을 유지시킴. 런타임 시점에 제거됨
      • SOURCE 와 마찬가지로 런타임 시점에는 결국 제거된 상태지만 소스 코드 없이 .class 파일만 제공되는 상황에서 애너테이션 정보를 제공하기 위해서 사용됨 (ex - jar 파일의 .class)
    • RUNTIME
      • 런타임 시점까지 애너테이션을 유지시킴
@Retention(RetentionPolicy.SOURCE)
@Retention(RetentionPolicy.CLASS)
@Retention(RetentionPolicy.RUNTIME)

애너테이션의 Retention 설정값이 SOURCE 의 경우 컴파일 후 바이트코드에서는 제거된다.

 

@Target

  • 애너테이션을 적용할 수 있는 유형을 설정하는 메타 애너테이션
  • TYPE, FIELD, METHOD, PARAMETER 등 다양한 유형을 지정할 수 있다.
@Target(ElementType.METHOD)
@Target(ElementType.PARAMETER)

 

@Inherited

  • @Inherited 이 적용된 애너테이션을 사용한 클래스를 상속한 서브클래스는 애너테이션 정보까지 상속받도록 하는 메타 애너테이션

'Java' 카테고리의 다른 글

Thread의 동기화 처리  (0) 2022.09.04
Demon Thread  (0) 2022.09.04
Thread 의 동시성과 병렬성  (0) 2022.09.01
JVM - Java Virtual Machine  (0) 2022.08.30
자바 소스 코드의 실행 과정  (0) 2022.08.30

동기화

  • 이전 작업이 완전히 완료된 후 다른 작업을 수행하는 것.
    • 반대로 이전 작업의 완료 여부와 상관없이 바로 다른 작업 명령을 수행하는 것을 '비동기화' 라고 한다.

 

멀티 쓰레드 환경에서의 비동기

class TestClass {
    int data = 3;

    void print(Thread thread) {
        System.out.println(thread.getName() + " : " + data);
        data += 1;
    }
}


@Test
@DisplayName("쓰레드 비동기 테스트")
void testThreadAsync() throws InterruptedException {
    TestClass testClass = new TestClass();
    Thread thread1 = new Thread(() -> testClass.print(Thread.currentThread()));
    thread1.setName("thread1");
    Thread thread2 = new Thread(() -> testClass.print(Thread.currentThread()));
    thread2.setName("thread2");
    Thread thread3 = new Thread(() -> testClass.print(Thread.currentThread()));
    thread3.setName("thread3");
    Thread thread4 = new Thread(() -> testClass.print(Thread.currentThread()));
    thread4.setName("thread4");

    // 실행 순서가 보장되지 않음
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();

    Thread.sleep(2000);
    System.out.println("최종결과 : " + testClass.data);
}

  • 기본적으로 멀티 쓰레드 환경에서는 비동기적으로 작업을 수행하지만 이는 특정 상태값을 공유하는 작업에서 의도치않은 문제를 야기할 수 있으므로 주의해야한다. 때문에 웹 어플리케이션같은 멀티 쓰레드 환경에서는 서로 다른 요청을 담당하는 여러 쓰레드가 같은 상태값을 공유함으로서 발생할 수 있는 문제를 방지하기 위해 기본적으로 무상태성을 권장한다.

 

멀티쓰레드 환경에서의 동기화

class SyncClass {
    int data = 3;
    Object lock = new Object();

    // printA 와 printB 는 같은 this 객체의 Lock 을 사용하므로 동시에 진행될 수 없다.
    synchronized void printA(Thread thread) {
        System.out.println(thread.getName() + " : " + data);
        data += 1;
    }
    synchronized void printB(Thread thread) {
        System.out.println(thread.getName() + " : " + data);
        data += 1;
    }

    // callA 와 callB 내부의 동기화 블록은 같은 객체의 Lock 을 사용하므로 동시에 진행될 수 없다.
    void callA(Thread thread) {
        synchronized (lock) {
            System.out.println(thread.getName() + " : " + data);
            data += 1;
        }
    }
    void callB(Thread thread) {
        synchronized (lock) {
            System.out.println(thread.getName() + " : " + data);
            data += 1;
        }
    }
    
    // printA 와 callA 내부의 동기화 블록은 다른 객체의 Lock을 사용하므로 동시에 진행 가능
}
  • 메서드를 동기화할 때는 동기화하고자 하는 메서드의 리턴 타입 앞에 synchronized 키워드를 붙여준다.
  • 일정 영역을 블록으로 동기화할 때에는 synchronized(object) {} 블록으로 감싸준다.
    • 메서드 전체를 동기화하지 않고 메서드 내부의 필요한 부분만 한정해서 동기화를 적용할 때 동기화 블록을 사용한다.
  • 객체마다 하나의 Lock 을 보유하고 있으며 해당 Lock 이 필요한 동기화 메서드 혹은 동기화 블록을 처리하는 쓰레드는 해당 Lock 을 가지고 작업을 처리하며 Lock 을 반납하기 전까지는 해당 Lock 이 필요한 다른 쓰레드들은 Lock 이 반납될 때까지 대기하게 된다.

 

'Java' 카테고리의 다른 글

애너테이션  (0) 2022.09.06
Demon Thread  (0) 2022.09.04
Thread 의 동시성과 병렬성  (0) 2022.09.01
JVM - Java Virtual Machine  (0) 2022.08.30
자바 소스 코드의 실행 과정  (0) 2022.08.30

Demon Thread

  • 프로세스의 모든 일반 쓰레드가 종료될 때 남은 작업에 상관없이 같이 종료되는 쓰레드
  • 가비지 컬렉터, 자동 저장 등 프로그램 종료 시점에 맞춰 같이 종료되어야 하는 작업에 사용됨

 

public static void main(String[] args) {
    Thread commonThread = new Thread(() -> {
        String threadName = Thread.currentThread().getName();
        IntStream.range(0, 3).forEach(idx -> execute(threadName));
        System.out.println(threadName + " 종료");
    });
    commonThread.setName("commonThread");

    Thread demonThread = new Thread(() -> {
        String threadName = Thread.currentThread().getName();
        IntStream.range(0, 5).forEach(idx -> execute(threadName));
        System.out.println(threadName + " 종료");
    });
    demonThread.setDaemon(true);
    demonThread.setName("demonThread");

    try {
        commonThread.start();       // 일반 쓰레드 실행

        Thread.sleep(2000);     	// 1초 뒤 데몬 쓰레드 실행
        demonThread.start();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("mainThread 종료");
}


public static void execute(String threadName) {
    System.out.println(threadName + " 실행");
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

'Java' 카테고리의 다른 글

애너테이션  (0) 2022.09.06
Thread의 동기화 처리  (0) 2022.09.04
Thread 의 동시성과 병렬성  (0) 2022.09.01
JVM - Java Virtual Machine  (0) 2022.08.30
자바 소스 코드의 실행 과정  (0) 2022.08.30

프로세스와 쓰레드

프로세스

  • 메모리 상에 로딩된 프로그램

쓰레드

  • 프로세스 내에서 실행되는 작업 단위
  • 하나의 프로세스 내에서 여러 쓰레드가 동시에 실행될 수 있다.
  • 프로세스는 최소 하나 이상의 쓰레드로 실행되며 자바의 경우 실행 시점에 main 쓰레드를 생성하여 작업을 실행한다.

 

쓰레드의 동시성

쓰레드의 동시성은 동시에 실행되는 것이 아닌 동시에 실행되는 것처럼 보이게하는 현상이다.

  • 쓰레드를 실행하는 주체는 CPU 의 코어로서 코어의 수가 쓰레드의 수보다 작을 경우 코어는 쓰레드를 번갈아가면서 실행된다. 이 때, 번갈아가면서 실행되는 주기는 사람이 인지할 수 없을 정도로 매우 짧아 사용자는 마치 동시에 실행되는 것처럼 느낀다. 이를 쓰레드의 동시성이라고 한다.

 

쓰레드의 병렬성

  • CPU 의 코어 수가 쓰레드의 수 이상일 때 각각의 코어는 하나의 쓰레드를 담당하여 실행, 즉 병렬적으로 처리한다.

'Java' 카테고리의 다른 글

애너테이션  (0) 2022.09.06
Thread의 동기화 처리  (0) 2022.09.04
Demon Thread  (0) 2022.09.04
JVM - Java Virtual Machine  (0) 2022.08.30
자바 소스 코드의 실행 과정  (0) 2022.08.30

JVM

  • 자바 가상 머신
  • 자바 바이트 코드를 OS에 맞는 바이너리 코드로 컴파일하여 실행
    • 자바는 플랫폼 독립적이지만 바이트 코드를 컴파일하는 JVM은 플랫폼 종속적이다.

 

JVM 의 구조

ClassLoader

  • .class 파일의 바이트 코드를 읽고 메모리에 저장한다.
  • 로딩, 링크, 초기화 순으로 진행하여 메모리의 각 영역에 데이터를 저장

 

Memory

Method 영역

  • 클래스 수준의 정보를 저장
    • 클래스 이름, 부모 클래스 이름, 메소드, 변수
    • 공유 자원으로서 쓰레드에 상관없이 사용 가능한 자원

Heap 영역

  • 객체를 저장하며 Method 영역의 자원과 마찬가지로 공유 자원이다.

Stack 영역

  • 쓰레드마다 런타임 스택을 생성하고 그 안에 메서드 호출마다 스택 프레임을 쌓는다. 쓰레드가 종료되면 런타임 스택도 제거된다.

PC Register

  • 쓰레드마다 쓰레드 내 현재 실행할 위치를 가리키는 포인터가 생성된다.

 

Execution Engine

Interpreter

  • 바이트 코드를 한 줄씩 바이너리 코드로 해석하여 실행

JIT Compiler 

  • 인터프리터의 효율을 높이기 위해 작동하는 컴파일러
  • 인터프리터가 반복되는 코드를 발견하면 해당 코드들을 모두 네이티브 코드로 컴파일한다.
    • 네이티브 코드로 컴파일된 코드는 인터프리터가 더 이상 해석하지 않고 바로 실행하여 속도가 향상된다.

Gavage Collector

  • 더 이상 참조되지 않는 객체를 모아서 메모리를 해제한다.

'Java' 카테고리의 다른 글

애너테이션  (0) 2022.09.06
Thread의 동기화 처리  (0) 2022.09.04
Demon Thread  (0) 2022.09.04
Thread 의 동시성과 병렬성  (0) 2022.09.01
자바 소스 코드의 실행 과정  (0) 2022.08.30

+ Recent posts