development/spring

자주 사용하는 Spring annotation 정리

bokshiri 2023. 6. 5. 14:03

  안녕하세요.

  개발자란 주어진 환경을 그대로 수용하는 수동적 존재가 아니라 끊임없이 자신이 사용하는 프레임워크와 라이브러리에 대해 왜? 라는 의문을 제기하며 그 원리를 파악해나가는 능동적 존재라 생각합니다. 프레임워크와 환경의 동작원리를 알고 코딩을 하는 것과, 기계적으로 암기된 형태를 짜맞추는 것은 결과에 있어 큰 차이를 만듭니다. 또한 풍부한 기초지식은 그것을 바탕으로 응용을 할 수 있는 발판이 되기 때문에 코딩뿐만 아니라 이론과 동작원리에 대한 학습을 게을리하지 않을 생각입니다. 오늘은 그 학습의 일환으로서 Spring을 사용하여 웹 개발시 자주 사용하는 어노테이션에 대해 정리해보고자 합니다.

오늘 다룰 내용은 다음과 같습니다. 어노테이션이 제공하는 기능과 흔히 하는 실수들에 대해 살펴보도록 하겠습니다.

  • @Data
  • @RequiredArgsConstructor
  • @Transactional
  • @Controller와 @RestController

1. @Data(Lombok)

흔히 Entity를 정의한 후 사용하는 어노테이션입니다. 어노테이션의 정의는 다음과 같습니다.

  @Data 어노테이션 하나로 우리는 @Getter, @Setter, @RequiredArgsConstructor, @Tostring, @EqualsAndHashCode 의 기능을 모두 사용할 수 있습니다. 여기서 한가지 재미있는 점은, @RequiredArgsConstructor로 인해 @NoArgsConstructor  어노테이션 없이도 기본생성자를 사용할 수 있다는 점입니다. @RequiredArgsConstructor는 final이나 @NonNull 이 붙어있는 필드에 자동으로 생성자를 만들어주는 역할을 하는데, 보통의 경우에 필드는

@Data
public class TestClass1 {
	
    private String testField1;
    
}

다음과 같은 형태로 작성하기 때문에, 인자 없는 기본 생성자가 만들어지게 되는 것입니다.

  간혹 협업을 하다보면 @Data 어노테이션을 사용하고 그 하위에 @Tostring이나 @EqualsAndHashCode를 사용하는 것을 목격할 때가 있습니다. @Data 어노테이션엔 위 항목이 모두 포함되어 있기 때문에 굳이 별도로 선언해 줄 필요가 없습니다.

 

2. @RequiredArgsConstructor

  @Data를 하면서 잠깐 언급하였던 어노테이션입니다. Spring에서 DI(dependency Injection)를 할 때 사용하는 방법은 총 3가지인데(Field Injection, Constructor Injection, Setter Injection) 그 중 @RequiredArgsConstructor 를 활용한 생성자 주입을 권장한다고 합니다. 우리는 다음과 같은 형태로 생성자 주입을 사용할 수 있습니다.

@Service
@Slf4j
@RequiredArgsConstructor
public class SampleService {
	
    private final SampleRepository sampleRepository;
    
}

앞서 언급한 것처럼 @RequiredArgsConstructor 는 @Nonnull 어노테이션이나 final로 선언된 필드에 대해 생성자를 자동으로 만들어주기 때문에 필드를 위와 같이 선언해놓으면 런타임시에 Spring Bean이 등록될 때 해당 필드가 생성자를 통해 자동으로 초기화되는 것입니다.

스프링의 다양한 DI 방식에 대해 더 살펴보실 분은 망나니 개발자님이 정리해놓으신 이 글

 

[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)

Spring 프레임워크의 핵심 기술 중 하나가 바로 DI(Dependency Injection, 의존성 주입)이다. Spring 프레임워크와 같은 DI 프레임워크를 이용하면 다양한 의존성 주입을 이용하는 방법이 있는데, 각각의 방

mangkyu.tistory.com

을 참조하시기 바랍니다.

 

3. @Transactional

  정확한 원리를 모른채 사용하다보니 가장 애를 먹었던 어노테이션입니다.. Spring에서는 PlatformTransactionManager를 Bean으로 등록해놓고 AOP 형태로 선언적 트랜잭션을 사용할 수 있도록 지원하고 있습니다. 따라서 개발자는 별도의 명시적 트랜잭션 관리 코드 없이도 @Transactional 어노테이션 하나로 트랜잭션을 제어할 수 있게 되었습니다. 하지만.. 동작 원리를 모르는 상태로 사용하게 되면 RuntimeException을 던져도 롤백이 되지 않는 문제상황에 직면할 수 있습니다. (저 또한 2~3시간을 삽질한 경험이 있습니다.)

  @Transactional은 옵션에서 전파속성과 격리 수준을 설정할 수 있습니다. 워낙 방대한 내용이기 때문에 위 글에서는 주의 사항 정도만 간단하게 다루도록 하겠습니다. 전파속성과 격리수준에 관한 내용은 망나니 개발자님이 잘 정리해주신 이 글

 

[Spring] Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등) - (2/3)

아래의 내용은 토비의 스프링의 2권 2장을 참고해서 작성하였습니다. 1. Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등) [ 전파 속성(Propagation) ] Spring이 제공하는

mangkyu.tistory.com

을 참조하시길 바랍니다.

  프로젝트를 진행하며 다음과 같은 문제 상황에 직면한 적이 있습니다. 기존에 하나의 트랜잭션으로 처리하던 로직을, 반복문 안에서 건건이 트랜잭션을 분리하여 처리해야 하는 상황이었습니다. 문제를 해결하기까지의 과정은 다음과 같습니다.

  1. 해당 서비스에서 private 제한자로 메소드를 분리한 후 해당 메서드에 @Transactional을 선언하고 전파속성을 REQUIRES_NEW 로 선언하여 분리를 하였습니다. - 1차 실패
  2. 분리한 메소드의 접근제한자를 public으로 변경한 후 호출하는 부모 메소드에도 @Transactional을 붙여주었습니다. - 2차 실패
  3. 부모메소드의 반복문안에서 호출하는 자식 메소드를 같은 서비스가 아닌 별도의 서비스로 이동시켜 별도의 빈으로 구성하였습니다. - 눈물겨운 성공

  Spring AOP는 기본적으로 proxy bean을 활용합니다. @Transactional이 선언된 메소드를 호출하게 되면 해당 메소드의  bean을 직접 호출하는 것이 아닌, bean을 감싸고 있는 proxy bean이 호출됩니다. proxy bean에서 트랜잭션을 명시적으로 시작하고 target bean의 메소드를 호출해준 후 해당 메소드가 종료되면 commit 처리를 해주는 것입니다. 따라서 호출하고자 하는 메소드는 반드시 public으로 선언하여야 합니다. (private은 외부에서 접근이 불가능하기 때문입니다.) 또한 트랜잭션을 분리하고자 하는 자식 메소드가 부모메소드와 같은 클래스 내에 있어서는 안됩니다. 두 메소드가 같은 bean 내에 선언되어 있으면, proxy bean은 부모 메소드가 호출될 때에만 트랜잭션을 처리하기 때문에 우리가 의도한 자식메소드의 트랜잭션 분리 처리가 이뤄지지 않게 됩니다.

  위 과정은 spring aop의 proxy형태 동작 원리를 몰라서 발생한 해프닝이었습니다. 정리하자면, 1. 부모메소드에 @Transactional 선언 2. 자식메소드를 별도의 bean으로 분리 후 메소드 정의 3. 메소드는 반드시 public으로 설정 하면 됩니다.

 

4. @Controller와 @RestController

  스프링을 사용하며 가장 많이 접하는 어노테이션입니다. spring에서는 기본적으로 DispatcherServlet이 모든 HTTP Request를 제일 앞단에서 받습니다. 요청을 받은 DispatcherServlet은 HandlerMapping을 통해 요청을 전달할 컨트롤러(서블릿)을 탐색한 후 HandlerAdapter에 전달합니다. 요청을 전달받은 HandlerAdapter는 adapter 패턴을 활용하여 적합한 Controller에 요청을 위임합니다. 여기서 Controller에 해당하는 Bean을 생성하기 위한 어노테이션이 바로 @Controller와 @RestController 입니다. 위 둘은 다음과 같은 차이가 있습니다.

  먼저 @Controller의 경우 요청을 전달받은 후 마지막에 viewResolver를 통해 화면(mvc패턴의 view)을 반환하는 역할을 합니다. 스프링을 사용해보신 분이라면 익숙하실 내용으로, model에 데이터를 담고 return을 통해 jsp나 thymeleaf를 반환할 때 사용합니다. @RestController@Controller와 @ResponseBody가 결합된 어노테이션으로, Json 형태의 데이터를 반환할 때 사용합니다. spring에서는 MessageConverter가 objectMapper를 활용하여 object를 json문자열로 반환하는 직렬화와 json문자열을 object로 변환하는 역직렬화를 제공합니다. 과거에는 화면상에서 json데이터를 전달할 때 그것을 받기 위해 @RequestBody를, 서블릿에서 json문자열을 반환하기 위해 @RestController나 @ResponseBody를 기계적으로 암기하여 사용하였었는데, 정확한 동작 원리를 알기 위해 별도로 학습을 진행하였습니다.

  먼저 스프링에서는 직렬화를 할 때 objectMapper를 활용하여 객체에 선언되어 있는 getter로 value를 읽어와 문자열을 만들어줍니다. 

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValueAsString(obj);

이와 대조되는 역직렬화의 경우, MessageConverter는 objectMapper를 활용하여 @RequestBody가 선언되어 있는 대상의 기본생성자로 객체를 생성한 후 getter메소드를 찾아 json 데이터의 key와 매핑을 한 후 reflection으로 데이터 바인딩을 합니다. 따라서 바인딩 클래스를 만들 때 @Setter를 굳이 선언하지 않더라도, @NoArgsConstructor와 @Getter만 선언되어 있다면 reflection을 통해 스프링이 자동으로 json -> object로 바인딩을 시켜주는 것입니다. 이를 코드상에서 명시적으로 사용하는 법은 다음과 같습니다.

ObjectMapper objectMapper = new ObjectMapper();
TestClass testClass = objectMapper.readValue(body, TestClass.class);

body는 json 문자열(String)을 전달하시면 되겠습니다.

'development > spring' 카테고리의 다른 글

테스트 주도 개발 - TDD란?  (0) 2023.08.01
Spring Boot 와 Spring Legacy  (2) 2023.07.18
QueryDSL - Introduction / Join / Dynamic Query  (4) 2023.02.15
JPA - JPQL과 QueryDSL 활용  (5) 2023.01.31
JPA를 활용한 기본 CRUD 구현  (7) 2023.01.28