앞선 글에서 우리는 Spring 기반 웹 애플리케이션에서 자바 객체가 JSON으로 직렬화되는 과정을 살펴보았습니다. 이번에는 그 반대 과정, 즉 JSON 데이터가 자바 객체로 역직렬화되는 과정을 자세히 알아보겠습니다. 이 과정 역시 주로 Jackson 라이브러리에 의해 처리되며, Spring Framework와 긴밀히 통합되어 있습니다.

역직렬화 과정

1. 요청 수신 및 컨트롤러 매핑

클라이언트로부터 JSON 데이터를 포함한 HTTP 요청이 서버에 도착하면 다음과 같은 과정을 거칩니다:

  1. 클라이언트는 일반적으로 Content-Type 헤더에 "application/json"을 포함하여 JSON 데이터를 전송합니다.
  2. 서버의 서블릿 컨테이너(예: Tomcat)가 요청을 받아 Spring의 DispatcherServlet에 전달합니다.
  3. DispatcherServlet은 HandlerMapping을 통해 적절한 컨트롤러 메소드를 찾습니다.
@RestController  
public class UserController {  
    @PostMapping("/user")  
    public User createUser(@RequestBody User user) {  
        return userService.save(user);  
    }  
}  

이 예제에서 `/user` 경로로 POST 요청이 오면, `createUser` 메소드가 실행됩니다.

2. @RequestBody 처리

Spring MVC는 메소드 파라미터에 @RequestBody 어노테이션이 있는 경우 RequestResponseBodyMethodProcessor를 사용하여 처리합니다.

이 프로세서는 다음 작업을 수행합니다:

  1. @RequestBody 어노테이션 확인
  2. 파라미터의 타입 분석
  3. Content-Type 확인

3. HttpMessageConverter 선택

Spring은 등록된 여러 HttpMessageConverter 중에서 적절한 컨버터를 선택합니다. JSON 요청의 경우 일반적으로 MappingJackson2HttpMessageConverter가 선택됩니다.

선택 과정은 다음과 같습니다:

  1. 등록된 HttpMessageConverter 순회
  2. 각 컨버터의 canRead(Class<?>, MediaType) 메소드 호출
  3. MediaType 일치 여부 확인
  4. 조건을 만족하는 첫 번째 컨버터 선택

4. ObjectMapper 사용

선택된 MappingJackson2HttpMessageConverter는 내부적으로 ObjectMapper를 사용하여 실제 역직렬화를 수행합니다.

주요 과정은 다음과 같습니다:

  1. ObjectMapper의 readValue() 메소드 호출
  2. JsonParser를 사용한 JSON 입력 처리
  3. DeserializationContext 설정
  4. JavaType 객체 생성 및 적절한 JsonDeserializer 선택
  5. BeanDeserializer를 통한 객체 속성 설정

```java
ObjectMapper objectMapper = new ObjectMapper();
User user = objectMapper.readValue(jsonString, User.class);
```

이는 ObjectMapper를 직접 사용하는 예제입니다. Spring에서는 이 과정이 내부적으로 처리됩니다.

5. JSON 파싱 및 객체 생성

Jackson은 JSON 데이터를 파싱하여 Java 객체를 생성합니다. 이 과정에서 다음과 같은 작업이 수행됩니다:

  1. JSON 토큰 읽기
  2. 객체의 기본 생성자 호출 또는 @JsonCreator 어노테이션이 붙은 생성자 사용
  3. JSON 필드와 Java 객체의 속성 매핑
  4. setter 메소드 호출 또는 필드에 직접 값 설정
public class User {  
    private String name;  
    private int age;  

    @JsonCreator  
    public User(@JsonProperty("name") String name, @JsonProperty("age") int age) {  
        this.name = name;  
        this.age = age;  
    }  

    // getters and setters  
}  

이 예제에서 Jackson은 @JsonCreator 어노테이션이 붙은 생성자를 사용하여 객체를 생성합니다.

6. 타입 변환

JSON의 데이터 타입과 Java 객체의 필드 타입이 다른 경우, Jackson은 적절한 타입 변환을 수행합니다.

  • 문자열 → 날짜
  • 숫자 → Enum
  • 문자열 → UUID 등
public class Event {  
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
    private Date eventDate;  

    @JsonDeserialize(using = CustomEnumDeserializer.class)  
    private EventType type;  

    // getters and setters  
}  

이 예제에서 `eventDate`는 지정된 패턴에 따라 문자열에서 Date 객체로 변환되며, `type`은 커스텀 Deserializer를 통해 변환됩니다.

7. 유효성 검증

Spring의 Validator 또는 Bean Validation(예: @Valid)을 사용하여 생성된 객체의 유효성을 검증할 수 있습니다.

@PostMapping("/user")  
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {  
    // ...  
}  

이 예제에서 @Valid 어노테이션은 User 객체의 유효성 검증을 트리거합니다.

개발자가 주의해야 할 사항

JSON 역직렬화 과정에서 개발자들이 자주 마주치는 문제와 그 해결책을 살펴보겠습니다:

  1. 생성자와 setter 메소드

Jackson은 기본적으로 기본 생성자 또는 setter 메소드를 사용하여 객체를 생성하고 값을 설정합니다. 이들이 없으면 역직렬화에 실패할 수 있습니다.

해결책:

  • 기본 생성자 추가
  • @JsonCreator와 @JsonProperty 사용
  • Jackson의 visibility 설정 변경
  1. 알 수 없는 속성 처리

JSON에 Java 클래스에 정의되지 않은 필드가 있을 경우 UnrecognizedPropertyException이 발생할 수 있습니다.

해결책:

  • @JsonIgnoreProperties(ignoreUnknown = true) 사용
  • ObjectMapper 설정 변경
@JsonIgnoreProperties(ignoreUnknown = true)  
public class User {  
    // ...  
}  
  1. 다형성 객체 처리

추상 클래스나 인터페이스의 구현체를 역직렬화할 때 타입 정보가 필요할 수 있습니다.

해결책:

  • @JsonTypeInfo와 @JsonSubTypes 사용
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")  
@JsonSubTypes({  
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),  
    @JsonSubTypes.Type(value = Cat.class, name = "cat")  
})  
public abstract class Animal { }  
  1. 순환 참조

양방향 관계가 있는 객체를 역직렬화할 때 순환 참조로 인한 문제가 발생할 수 있습니다.

해결책:

  • @JsonManagedReference와 @JsonBackReference 사용
  • DTO 패턴 적용
  1. 날짜/시간 형식

날짜와 시간 데이터의 형식이 일치하지 않으면 파싱 오류가 발생할 수 있습니다.

해결책:

  • @JsonFormat 사용
  • 커스텀 Deserializer 구현
public class Event {  
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
    private Date eventDate;  
}  
  1. Immutable 객체 처리

final 필드를 가진 Immutable 객체의 경우 일반적인 역직렬화 방식으로는 처리가 어려울 수 있습니다.

해결책:

  • @JsonCreator와 @JsonProperty 조합 사용
  • Builder 패턴과 함께 @JsonDeserialize 사용
@JsonDeserialize(builder = ImmutableUser.Builder.class)  
public class ImmutableUser {  
    private final String name;  
    private final int age;  

    private ImmutableUser(Builder builder) {  
        this.name = builder.name;  
        this.age = builder.age;  
    }  

    public static class Builder {  
        private String name;  
        private int age;  

        @JsonProperty("name")  
        public Builder withName(String name) {  
            this.name = name;  
            return this;  
        }  

        @JsonProperty("age")  
        public Builder withAge(int age) {  
            this.age = age;  
            return this;  
        }  

        public ImmutableUser build() {  
            return new ImmutableUser(this);  
        }  
    }  
}  

결론

Spring과 Jackson의 조합은 JSON 데이터의 역직렬화 과정을 매우 유연하고 강력하게 만듭니다. 그러나 이 과정을 깊이 이해하고 있으면, 복잡한 데이터 구조를 다룰 때 발생할 수 있는 다양한 문제들을 효과적으로 해결할 수 있습니다.

특히 도메인 모델이 복잡한 경우, API 계층에서는 DTO 패턴을 사용하여 역직렬화를 단순화하고, 도메인 객체와 API 요청/응답을 명확히 분리하는 것이 좋습니다. 이를 통해 역직렬화 관련 문제를 더 쉽게 관리하고, API의 버전 관리도 용이해집니다.

참고 자료:

  1. Spring Framework Documentation
  2. Jackson Project Documentation
  3. Java Reflection API Documentation
  4. HTTP Specification (RFC 7231)

스프링 기반의 웹 애플리케이션을 개발하다 보면, JSON 형태의 데이터와 자바 객체 간의 변환이 자연스럽게 이루어집니다. 이 과정은 주로 Jackson 라이브러리에 의해 처리되는데, 오늘은 그 내부 동작 원리를 자세히 살펴보겠습니다. 특히 이번 글에서는 자바 객체가 JSON으로 직렬화되는 과정에 초점을 맞추겠습니다.

직렬화 과정

  1. 요청 처리 및 컨트롤러 실행

클라이언트로부터 JSON 응답을 요구하는 요청이 서버에 도착하면, 다음과 같은 과정을 거칩니다:

  1. 클라이언트는 일반적으로 Accept 헤더에 "application/json"을 포함하여 JSON 응답을 요청합니다.
  2. 서버의 서블릿 컨테이너(예: Tomcat)가 요청을 받아 Spring의 DispatcherServlet에 전달합니다.
  3. DispatcherServlet은 HandlerMapping을 통해 적절한 컨트롤러 메소드를 찾아 실행합니다.
@RestController  
public class UserController {  
    @GetMapping("/user/{id}")  
    public User getUser(@PathVariable Long id) {  
        return userService.findById(id);  
    }  
}  

이 예제에서 `/user/{id}` 경로로 요청이 오면, `getUser` 메소드가 실행되어 `User` 객체를 반환합니다.

  1. @ResponseBody 처리

Spring MVC는 컨트롤러 메소드의 반환값을 처리하기 위해 HandlerMethodReturnValueHandler를 사용합니다. @ResponseBody 어노테이션이 있는 경우(또는 @RestController를 사용한 경우), RequestResponseBodyMethodProcessor가 선택됩니다.

이 프로세서는 다음 작업을 수행합니다:

  1. @ResponseBody 어노테이션 확인
  2. 반환된 객체의 타입 분석
  3. MediaType 결정

MediaType 결정 과정은 다음과 같습니다:

  1. Accept 헤더 파싱
  2. @Produces 어노테이션 확인 (또는 @GetMapping의 produces 속성)
  3. ContentNegotiationManager를 통한 최종 MediaType 결정
  4. 여기서 부분 일치도 없는 상황에서 완전 불일치가 발생한다면 406 Not Acceptable을 반환합니다.
@GetMapping(value = "/user/{id}", produces = MediaType.APPLICATION\_JSON\_VALUE)  
public User getUser(@PathVariable Long id) {  
    return userService.findById(id);  
}  

기본값의 경우 application/json 입니다. 이 예제에서는 명시적으로 JSON 응답을 생성하도록 지정했습니다.

@ResponseBody의 기능이 하나 더 있는데 @ResponseBody는 반환값이 View 이름이더라도 ViewResolver를 통한 처리를 건너 뛰고 HttpMessageConverter를 사용합니다.

  1. HttpMessageConverter 선택

Spring은 여러 HttpMessageConverter를 등록하고 있으며, 이들 중 적절한 컨버터를 선택합니다. JSON 응답의 경우 일반적으로 MappingJackson2HttpMessageConverter가 선택됩니다.

선택 과정은 다음과 같습니다:

  1. 등록된 HttpMessageConverter 순회
  2. 각 컨버터의 canWrite(Class<?>, MediaType) 메소드 호출
  3. MediaType 일치 여부 확인
  4. 조건을 만족하는 첫 번째 컨버터 선택

주의할 점은 컨버터의 등록 순서가 중요하다는 것입니다. 사용자 정의 컨버터를 사용하려면 이 점을 고려해야 합니다.

  1. ObjectMapper 사용

선택된 MappingJackson2HttpMessageConverter는 내부적으로 ObjectMapper를 사용하여 실제 직렬화를 수행합니다.

주요 과정은 다음과 같습니다:

  1. ObjectMapper의 writeValue() 메소드 호출
  2. SerializerProvider를 통한 직렬화 컨텍스트 설정
  3. JsonGenerator를 사용한 JSON 출력 생성
  4. JavaType 객체 생성 및 적절한 JsonSerializer 선택
  5. BeanSerializer를 통한 객체 속성 순회 및 직렬화
ObjectMapper objectMapper = new ObjectMapper();  
String json = objectMapper.writeValueAsString(user);  

이는 ObjectMapper를 직접 사용하는 예제입니다. Spring에서는 이 과정이 내부적으로 처리됩니다.


POJO의 경우 BeanSerializer가 선택됩니다. 이 때 BeanSerializer는 객체의 속성들을 순회하며 각 속성에 대한 이름을 JSON 필드 이름으로 변환하고 getter 또는 필드 직접 접근을 통해 속성 값을 가져옵니다.


getter 또는 필드 직접 접근 이라는 설명에서 알 수 있듯이 필드가 private인데 getter가 존재하지 않는다면 직렬화를 할 수 없습니다. 이 때 @JsonProperty 어노테이션을 사용하면 getter가 없어도 private 필드를 직렬화 할 수 있습니다.


5. 리플렉션 사용

Jackson은 Java 리플렉션 API를 사용하여 객체의 구조를 분석하고 데이터를 추출합니다.

public class User {  
    private String name;  

    @JsonIgnore  
    private String password;  

    @JsonFormat(pattern = "yyyy-MM-dd")  
    private Date birthDate;  

    // getters and setters  
}  

이 예제에서 Jackson은 리플렉션을 통해 각 필드의 정보와 어노테이션을 분석합니다.

  1. JSON 트리 구성 및 문자열 생성

ObjectMapper는 분석된 데이터를 바탕으로 내부적으로 JSON 트리 구조(JsonNode)를 구성한 후, 이를 JSON 문자열로 변환합니다.

  1. 응답 전송

생성된 JSON 문자열은 HTTP 응답 본문에 포함되어 클라이언트에게 전송됩니다.

response.setContentType(MediaType.APPLICATION\_JSON\_VALUE);  
response.getWriter().write(jsonString);  

이는 low-level에서의 응답 전송 예제입니다. Spring MVC에서는 이 과정이 자동으로 처리됩니다.

 

개발자가 주의해야 할 사항

 

Jackson을 사용한 JSON 직렬화 과정에서 개발자들이 자주 마주치는 문제와 그 해결책을 살펴보겠습니다:

  1. 접근 제어자와 Getter 메소드

Jackson은 기본적으로 객체의 Getter 메소드나 public 필드를 통해 직렬화를 수행합니다. private 필드에 대한 Getter가 없으면 해당 필드는 직렬화되지 않습니다.

public class User {
    private String name; // 이 필드는 직렬화되지 않습니다!
}

해결책:

  • Getter 메소드 추가
  • @JsonProperty 어노테이션 사용
  • Jackson의 visibility 설정 변경
public class User {
    @JsonProperty
    private String name; // 이제 이 필드는 직렬화됩니다.
}
  1. 순환 참조

객체 간 양방향 관계가 있을 경우, 무한 재귀로 인한 StackOverflowError가 발생할 수 있습니다.

해결책:

  • @JsonManagedReference와 @JsonBackReference 사용
  • @JsonIgnore 사용
  • DTO(Data Transfer Object) 패턴 적용
public class Parent {
    @JsonManagedReference
    private List<Child> children;
}

public class Child {
    @JsonBackReference
    private Parent parent;
}
  1. Lazy Loading

JPA의 지연 로딩(Lazy Loading)된 필드에 접근할 때, 세션이 닫혀 있으면 LazyInitializationException이 발생할 수 있습니다.

해결책:

  • @JsonIgnore 사용
  • DTO 패턴 적용
  • Jackson-Hibernate 모듈 사용
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate5Module());
  1. 날짜/시간 형식

날짜/시간 필드의 형식이 일관되지 않으면 클라이언트 측에서 파싱 오류가 발생할 수 있습니다.

해결책:

  • @JsonFormat 어노테이션 사용
public class Event {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date eventDate;
}
  1. 불필요한 필드 제외

특정 상황에서 일부 필드를 제외해야 할 때가 있습니다.

해결책:

  • @JsonIgnore 사용
  • @JsonIgnoreProperties 클래스 레벨에서 사용
@JsonIgnoreProperties({"password", "secretKey"})
public class User {
    private String username;
    private String password; // 이 필드는 JSON에 포함되지 않습니다.
}
  1. 성능 최적화

대량의 객체 직렬화 시 성능 저하가 발생할 수 있습니다.

해결책:

  • @JsonInclude를 사용하여 null 값 제외
  • Jackson의 ObjectMapper 설정 최적화
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
    private String name;
    private String email; // null이면 JSON에 포함되지 않습니다.
}
  1. 타입 정보 포함

다형성 객체의 타입 정보가 손실될 수 있습니다.

해결책:

  • @JsonTypeInfo 어노테이션 사용
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Animal { }

이러한 주의사항들을 고려하면서 개발하면, Jackson을 이용한 JSON 직렬화 과정에서 발생할 수 있는 많은 문제들을 예방할 수 있습니다. 특히 복잡한 도메인 모델을 다룰 때는 DTO 패턴의 사용을 고려하는 것이 좋습니다. 이를 통해 도메인 객체와 API 응답을 명확히 분리하고, 직렬화 관련 문제를 더 쉽게 관리할 수 있습니다.

결론

Spring과 Jackson의 조합은 복잡한 직렬화 과정을 개발자로부터 숨기고, 편리한 API를 제공합니다. 그러나 이 과정을 이해하는 것은 더 효과적인 API 설계와 문제 해결에 도움이 됩니다.

다음 글에서는 JSON에서 자바 객체로의 역직렬화 과정을 다루겠습니다.

 

 

 

참고 자료:

  1. Spring Framework Documentation
  2. Jackson Project Documentation
  3. Java Reflection API Documentation
  4. HTTP Specification (RFC 7231)

서블릿 개발을 하면서 나는 항상 HttpServlet을 상속받고 @WebServlet 어노테이션을 붙이는 것이 당연하다고 생각해왔습니다. 그러나 최근 이 두 요소의 실제 역할과 관계에 대해 의문이 들어 공부해 보기로 했습니다.

 

먼저, Java Servlet 스펙을 살펴보겠습니다. 이 스펙에 따르면, 모든 서블릿은 javax.servlet.Servlet 인터페이스를 구현해야 합니다. 이 인터페이스는 다음과 같은 핵심 메소드를 정의합니다.

public interface Servlet {
    public void init(ServletConfig config) throws ServletException;
    public ServletConfig getServletConfig();
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
    public String getServletInfo();
    public void destroy();
}

이 메소드들은 서블릿의 생명주기와 요청 처리를 담당합니다.

그렇다면 HttpServlet은 무엇일까요? HttpServlet은 Servlet 인터페이스를 구현한 GenericServlet의 하위 클래스입니다. HttpServlet은 HTTP 프로토콜에 특화된 추가 기능을 제공합니다. 특히, service 메소드를 오버라이드하여 HTTP 메소드(GET, POST 등)에 따라 적절한 do* 메소드를 호출합니다.

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        doGet(req, resp);
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
    }
    // ... 기타 HTTP 메소드들
}

이렇게 함으로써 개발자는 각 HTTP 메소드에 대한 처리를 쉽게 구현할 수 있습니다.

그렇다면 @WebServlet은? 놀랍게도 이 어노테이션은 서블릿의 기능과는 직접적인 관련이 없었습니다. Java EE 6에서 도입된 이 어노테이션은 서블릿의 선언적 등록을 위한 것입니다.

@WebServlet의 주요 속성들:

  • name: 서블릿의 이름
  • urlPatterns: 서블릿에 매핑될 URL 패턴
  • loadOnStartup: 서블릿의 로드 순서
  • initParams: 초기화 파라미터
  • asyncSupported: 비동기 처리 지원 여부

예를 들어:

@WebServlet(name = "MyServlet", urlPatterns = {"/hello"}, loadOnStartup = 1)
public class MyServlet extends HttpServlet {
    // 서블릿 로직
}

이 어노테이션은 사실상 web.xml의 <servlet> 및 <servlet-mapping> 요소를 대체합니다.

그렇다면 @WebServlet 없이는 서블릿을 등록할 수 없을까요? 그렇지 않습니다. 전통적인 web.xml을 사용하거나, Servlet 3.0부터 도입된 프로그래밍 방식의 서블릿 등록을 사용할 수 있습니다.

@WebListener
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext sc = sce.getServletContext();
        ServletRegistration.Dynamic servlet = sc.addServlet("MyServlet", new MyServlet());
        servlet.addMapping("/hello");
    }
}

이 과정을 통해 저는 서블릿의 각 구성 요소가 각자의 역할을 가지고 있음을 깨달았습니다. Servlet 인터페이스는 기본 구조를, HttpServlet은 HTTP 특화 기능을, @WebServlet은 편리한 설정을 제공합니다.

 

나름의 결론을 내려보도록 하겠습니다. @WebServlet은 필수가 아닙니다. 하지만 xml이나 프로그래밍적으로 설정을 하는 것보다 많은 편리함을 제공해 주고 있습니다. 따라서 사용하는 것이 좋다고 생각합니다.

 

참고 자료:

  1. Java Servlet Specification 4.0: https://jcp.org/en/jsr/detail?id=369
  2. JavaDoc for HttpServlet: https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServlet.html
  3. Java EE 6 Tutorial: https://docs.oracle.com/javaee/6/tutorial/doc/
  4. Java Servlet API: https://docs.oracle.com/javaee/7/api/javax/servlet/package-summary.html

스프링으로 개발을 하다 보면 SELECT 쿼리만을 필요로 하는 부분에 @Transactional(readOnly = true) 어노테이션을 사용하는 경우가 많습니다. 그런데 데이터베이스 관점에서 보면 단순히 SELECT 하는 쿼리라면 트랜잭션이 필요 없습니다. 그렇다면 @Transactional(readOnly = true)는 왜 사용하는 걸까요? 그냥 해당 어노테이션 없이 사용해도 되지 않을까요?

@Transactional 이해하기

우선 @Transactional에 대해 간단히 설명드리겠습니다.

@Transactional은 스프링 프레임워크에서 제공하는 선언적 트랜잭션 관리를 위한 어노테이션입니다. 이 어노테이션을 메서드나 클래스에 적용하면, 해당 메서드 또는 클래스의 모든 public 메서드가 트랜잭션 내에서 실행됩니다.
(claude)

@Transactional은 어노테이션 기반으로 트랜잭션 관리를 쉽게 도와줍니다. 이 어노테이션은 프록시를 기반으로 동작하며, 주요 특징은 다음과 같습니다:

  1. 프록시 생성: 스프링은 @Transactional이 적용된 빈에 대해 프록시를 생성합니다. 이 프록시는 원본 객체를 감싸고 있으며, 메서드 호출을 가로채서 트랜잭션 로직을 추가합니다.
  2. 트랜잭션 매니저: 프록시는 PlatformTransactionManager를 사용하여 트랜잭션을 관리합니다. 이 매니저는 트랜잭션의 시작, 커밋, 롤백을 담당합니다.
  3. AOP (Aspect-Oriented Programming): @Transactional의 동작은 AOP를 기반으로 합니다. 트랜잭션 관리는 횡단 관심사로 처리되며, 이를 통해 비즈니스 로직과 트랜잭션 처리 로직을 분리할 수 있습니다.
  4. 트랜잭션 전파: @Transactional은 다양한 전파 옵션을 가지고 있습니다. 기본값은 REQUIRED로, 이미 진행 중인 트랜잭션이 있으면 그 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
  5. 예외 처리: 기본적으로 런타임 예외가 발생하면 트랜잭션이 롤백됩니다. 체크 예외는 롤백되지 않지만, rollbackFor 속성을 사용하여 롤백 동작을 커스터마이즈할 수 있습니다.
  6. 트랜잭션 동기화: 스프링은 ThreadLocal을 사용하여 트랜잭션 컨텍스트를 관리합니다. 이를 통해 동일한 스레드 내에서 여러 데이터 액세스 작업을 하나의 트랜잭션으로 묶을 수 있습니다.

 

(출처:

https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html

)

readOnly 옵션의 역할

이제 readOnly 옵션에 대해 자세히 살펴보겠습니다.

This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.
(Javadoc)

readOnly 옵션은 실제로 데이터베이스에 대한 쓰기를 물리적으로 방지하지는 않습니다. 하지만 힌트를 제공함으로써 데이터베이스 수준의 최적화를 할 수 있습니다. 특히 Hibernate에서는 최적화 효과가 있습니다.

readOnly 옵션이 설정된 트랜잭션에서 Hibernate는 영속성 컨텍스트의 동작을 크게 최적화합니다:

  • 더티 체킹의 비활성화: 엔티티의 변경사항을 추적하지 않음으로써 성능이 향상됩니다.
  • 엔티티의 초기 상태 스냅샷 관리 최소화: 메모리 사용량이 감소합니다.
  • FlushMode를 MANUAL로 설정: 불필요한 데이터베이스 동기화가 방지됩니다.
  • 2차 캐시에 엔티티를 저장하지 않음: 캐시 오염을 막을 수 있습니다.
  • 쓰기 지연 저장소 비활성화: 추가적인 메모리 최적화가 이루어집니다.

그러나 이러한 최적화 효과를 얻기 위해서는 주의할 점이 있습니다. readOnly 옵션은 Propagation.REQUIRED 또는 REQUIRES_NEW일 때만 효과적으로 동작합니다.

읽기 작업에서 @Transactional(readOnly = true)의 필요성

데이터베이스 관점에서는 순수한 읽기 작업에 대해 트랜잭션을 사용하는 것은 일반적으로 오버헤드를 증가시킵니다. 그러나 일부 상황에서는 읽기 작업에도 트랜잭션이 필요할 수 있습니다:

  • 특정 시점의 일관된 데이터 스냅샷이 필요한 경우
  • 특정 격리 수준을 보장해야 하는 경우

또한 Hibernate와 같은 ORM을 사용할 때 readOnly 옵션은 앞서 설명한 최적화를 제공합니다. 이는 특히 대용량 데이터를 읽을 때 유용할 수 있습니다. MySQL의 InnoDB 엔진은 readOnly 트랜잭션에 대해 특정 최적화를 수행해주기도 합니다.

결론

단순 조회 쿼리에도 @Transactional(readOnly = true)를 사용하는 것이 여러 이점이 있습니다. 그러나 최종적으로는 항상 성능 테스트를 통해 결정을 내리는 것이 좋습니다.

참고 자료

+ Recent posts