Skip to content
Polymorlog

java, spring 환경에서 객체를 어떻게 생성할 것인가

java, spring, object10 min read

객체 생성 방식에 대한 고민

팀 내 객체 생성 방식이 제각각인 상황을 목격하게 되었다. 프로젝트의 복잡성이 날로 증가하면서 어떠한 변경이 추가될 떄, 일관되지 않은 객체 생성 방식 때문에 수정이 번거로워 진다. 이를 어떻게 하면 일관된 방식으로 통일할 수 있을지에 대한 고민에서 이 글을 작성하게 되었다.

단순하지만 근본적인 질문에서 시작해보자. Java로 개발할 때 객체를 어떻게 생성해야 할까? 여기서 말하는 객체가 모호하게 들린다면, 클래스의 인스턴스를 어떻게 생성할 것인가로 바꿔 생각해도 좋다.

논의의 범위를 좀 더 구체적으로 좁혀보자면, 스프링 부트 기반의 웹 애플리케이션을 개발할 때의 객체 생성 방식에 대해 이야기해보고자 한다.

객체를 생성하게 되는 시점들

스프링으로 개발하면서 객체를 생성하게 되는 시점 두 가지

  • 개발자가 명시적으로 의식하면서 처리하는 경우로, 주로 레이어 간 개념적인 클래스를 생성할 때이다.
    1// 서비스 레이어에서 엔티티를 DTO로 변환하는 경우
    2OrderDto orderDto = new OrderDto(order.getId(), order.getCustomerName());
    3
    4// 레포지토리에서 조회한 결과를 도메인 객체로 변환하는 경우
    5Order order = Order.createFrom(orderEntity);
  • 잘 의식하지 않고 자동화 되는 부분도 있다. 보통 프레임워크나 라이브러리에 의해 자동화되어 처리되는 경우다. 주로 외부에서 애플리케이션으로 데이터가 유입될 때 발생한다.
    자동으로 객체 생성이 되는 경우
    1// HTTP 요청 바디를 객체로 변환하는 경우
    2@PostMapping("/orders")
    3public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
    4// Jackson이 자동으로 OrderRequest 객체 생성
    5}
    6
    7// MyBatis resultType 매핑
    8@Select("SELECT id, name, price FROM products")
    9List<Product> findAll(); // MyBatis가 자동으로 Product 객체 생성
    10
    11// JPA 엔티티 조회
    12@Entity
    13public class User {
    14@Id
    15private Long id;
    16private String name;
    17
    18// JPA는 기본 생성자 필요
    19protected User() {}
    20}
  • 이때 리플렉션의 도움을 받을 때도 있고, 생성자를 사용할 수도, 팩토리 메서드를 사용할 수도 있다.
  • 이러나 저러나 자바 문법 아래서 근본적으로는 생성자가 객체를 생성하는 역할을 수행한다. 따라서 생성자에 따른 생성 방식을 잘 확인해야 한다.

문제는 의식하지 않는 곳에서 발생

생성자를 건드리기 시작하면서 문제가 발생한다

  • 예를 들어 클래스 레벨로 @Builder를 선언하면 무슨 문제가 생길까. 기본 설정에서 대분 JSON 역직렬화를 위해 jackson이 사용된다.
    • 이 경우 객체 생성은 리플렉션을 위해 기본 생성자를 필요로 한다.
    • 빌더를 클래스 레벨로 생성하는 순간 생성자가 생기므로 기본 생성자가 사라진다.
    • 이러면 요청을 파싱하지 못하는 오류가 발생한다.
  • mybatis도 유사한 제약이 있다. resultType으로 선언된 클래스가 생성자가 없다면 쿼리의 결과 행과 멤버 변수를 꼭 일치 시켜야만 한다. 변수를 일치시키지 않게 하기 위해서는 리플렉션을 사용할 수 있도록 기본 생성자를 재공해야 한다.
  • JPA 또한 @Entity로 선언된 객체는 매핑을 위해 반드시 기본 생성자가 필요하다.
  • 내가 자주 사용하는 의존성을 기준으로 생성자의 필요 여부를 간단히 정리해 보았다.
NameConstructorDetail
MyBatis기본 생성자 (no-args)ResultTypeParameterType으로 객체 전달 시 필요
JPA기본 생성자 (no-args), 필요한 필드를 포함한 생성자 (선택)@Entity 클래스는 반드시 기본 생성자 필요 (protected 이상의 접근 제어자), 다른 생성자가 있더라도 기본 생성자 필수, 영속성 컨텍스트가 프록시 객체 생성 시 필요
Jackson기본 생성자 (no-args) 또는 @JsonCreator로 지정된 생성자JSON deserialize 시 기본 생성자 필요, 또는 @JsonCreator + @JsonProperty로 지정된 생성자 사용 가능

그럼 기본 생성자를 다 생성하면 되지 않나.

  • 그렇다고 기본 생성자를 public으로 죄다 열어 주게 되면 계속해서 객체를 생성하는 방식이 왜곡되기 시작한다.
    • 모든 필드가 null인 객체에 값을 주입하기 위해 setter로 값을 지정하는 로직을 많이 보았을 것이다.
    • 이때 setter는 아무 의미가 없고, 단지 개발자의 편의를 위해 생성되었을 뿐이다. 필드 갯수만큼 나열되는 setter 메서드에서는 에서는 뭐가 맞고 틀린 것인지, 값을 setter에 넘기는 것이 어떤 의미를 가지는 것인지 알기도 어렵다.
      안티 패턴: setter를 통한 객체 생성
      Order order = new Order(); // 모든 필드 null
      order.setId(1L);
      order.setCustomerName("John");
      order.setStatus(OrderStatus.PENDING);
      order.setItems(items);
      // 어떤 필드가 필수이고 어떤 필드가 선택적인지 알 수 없음

협업을 위한 규칙을 마련해 보자면

깊게 생각하기 어렵다면 / 빌더 쓴다면 private으로 써라

  • 이펙티브 자바에서는 가장 먼저 객체 생성에 관련한 item들이 등장한다. 여기서 제안하는 두 가지 규칙을 맹목적으로 따르다 보면 대다수의 상황에서는 해결이 된다고 본다. 물론 하다보면 분명히 예외가 필요한 지점은 생기겠지만, 일단 펼쳐놓은 것을 막는 것보다 막아둔 것을 펼치는 것이 더 쉽다.
권장 객체 생성 패턴
public class Product {
private final String name;
private final BigDecimal price;
private final ProductCategory category;
@Builder(access = AccessLevel.PRIVATE)
private Product(String name, BigDecimal price, ProductCategory category) {
this.name = Objects.requireNonNull(name, "상품명은 필수입니다");
this.price = Objects.requireNonNull(price, "가격은 필수입니다");
this.category = Objects.requireNonNull(category, "카테고리는 필수입니다");
}
// 의미있는 이름의 팩토리 메서드
public static Product createNewProduct(String name, BigDecimal price, ProductCategory category) {
return Product.builder()
.name(name)
.price(price)
.category(category)
.build();
}
// 특수한 상황을 위한 팩토리 메서드
public static Product createGiftProduct(String name, ProductCategory category) {
return Product.builder()
.name(name)
.price(BigDecimal.ZERO)
.category(category)
.build();
}
}

빌드를 막는 규칙이 필요

여러 개발자가 협업하여 지속 가능한 소프트웨어를 만들기 위해서는 명확한 컨벤션이 필요하다. 그리고 이 컨벤션은 빌드 실패라는 강제성을 가질 때만 실효성 있게 지켜질 수 있다.

따라서 빌드 전 단계에서 수행되는 각종 검사 도구들을 통해 검증 가능한 규칙들을 마련해야 한다.

클래스 생성은 누구나 쉽게 할 수 있고, 그에 따른 객체 생성 방식도 매우 다양하게 구현될 수 있다. 경험상 이러한 다양성에 일관된 제약을 두는 것은 결코 쉬운 일이 아니다.

현재의 규칙이 완벽하든 그렇지 않든, 통일된 규칙의 존재는 향후 코드 수정을 용이하게 만들어 줄 것이다 (맨먼스 미신에서 언급된 '개념적 일관성'과 일맥상통한다). 끊임없는 변경과 싸우는 우리 개발자들이 객체 생성 방식을 깊이 고민하고 제약해야 하는 이유가 바로 여기에 있다.

© 2024 by Polymorlog. All rights reserved.
Theme by LekoArts