Inner class를 활용한 DTO 리팩토링

728x90

1. 상황

쇼핑몰  프로젝트를 구현하면서 api가 많아짐에 따라 그만큼 필요한 DTO의 수도 걷잡을 수 없이 많아지고 있었다.

 

그래서 처음에는 이를 최대한 알아보기 쉽게 구분하고자 request의 경우 in, response의 경우 out으로 나눠서 관리하였다.

 

하지만 모든 api가 request, response 두 가지만 존재하는 것은 아니었다.

request의 경우 특정 필드의 경우 key, value 형태의 리스트가 요구되기도 했고, 이에 따라 새로운 Dto를 추가해야 하는 상황이 발생했다.

 

 

2. 개선 전

 

클라이언트와 위와 같은 형태로 request를 보내주길 바라며 아래와 같은 코드를 작성했었다.

 
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class CreateProductRequestDto {
    private String productName;
    private Integer price;
    private Integer stock;
    
    ...

    private List<ProductImageDto> productImageUrls;
}
 

 

productImageUrls는 이미지의 url을 담을 String 필드와 해당 이미지가 썸네일인지 아닌지를 판단하는 Boolean 필드로 나눠져야 했으므로, 이 상황에서 나는 또 하나의 DTO를 만들어야 했다.

 

public class ProductImageDto {
    private String productImageUrl;
    private boolean thumbnail;

    public ProductImageDto(String productImageUrl, boolean thumbnail) {
        this.productImageUrl = productImageUrl;
        this.thumbnail = thumbnail;
    }

}

 

하지만 이 Dto는 request도, response도 아닌 유형이므로 in, out 어디에도 위치시키기 애매했다.

이런 케이스의 Dto가 생각보다 많이 필요했고, 너무 많은 Dto가 생산되는 탓에 관리도 힘들고 서로 연관된 정보를 처리하기에 복잡해지는 문제가 있었다.

 

이러한 상황을 해결하기 위해 조언을 구하다가 Inner Class를 도입하여 간소화하는 방식을 알게 되었다.

 

 

3. 개선 후

3.1 Inner Class란?

클래스 내부에 또 다른 클래스를 선언하는 방식이다.

내부에 선언된 Inner Class는 그걸 감싸고 있는 외부 클래스에 선언된 필드나 메서드에 접근이 가능하여 외부 클래스의 보조 기능을 구현하는데 적합하다.

 

Product가 제품 정보를 관리하는 클래스이고 InnerClass가 제품의 이미지를 처리하는 클래스라면, 제품 정보와 밀접하게 관련된 이미지를 효율적으로 관리할 수 있게 해 준다.

 

public class CreateProductRequestDto {
    private String productName;

    private Integer price;

    private Integer stock;

	...

    private List<ProductImageDto> productImageUrls;

    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @Builder
    public static class ProductImageDto {
        private String productImageUrl;
        private boolean thumbnail;
    }
}

 

3.2 정적 Inner Class

다만, 나의 경우에 Inner Class가 외부 클래스에 함부로 접근하지 못하게 하고 싶었기 때문에 static으로 선언했다.

정적 Inner Class로 선언하면 외부 클래스의 static 멤버 변수만 참조할 수 있기 때문이다.

 

이렇게 하면 외부 클래스의 인스턴스가 필요하지 않으므로, 외부 클래스 인스턴스 없이도 바로 정적 Inner Class의 인스턴스를 생성할 수 있다.

 

public class Main {
    public static void main(String[] args) {
        // 정적 Inner Class 인스턴스 생성 (외부 클래스 인스턴스 없이)
        CreateProductRequestDto.ProductImageDto productImage = new CreateProductRequestDto.ProductImageDto();
        
        ...
        
        // 정적 클래스는 외부 클래스의 인스턴스에 접근할 수 없으므로, 메소드가 외부 필드에 접근하지 않음
        // 예를 들어 displayProductDetails 메소드가 없다면, 이를 호출할 수 없다.
        System.out.println("Product Image URL: " + productImage.getProductImageUrl());
        System.out.println("Is Thumbnail: " + productImage.isThumbnail());
    }
}

 

외부 클래스와의 논리적 관계만 유지하고 서로 독립적인 동작을 수행할 수 있도록 하여 불필요한 의존성을 줄이되, 굳이 새로운 Dto를 만들어야 하는 상황을 최소화했다.

 

  • 동일한 static 멤버들은 사용 가능
  • static의 특징에 따라 외부 인스턴스 멤버의 직접 참조 불가

 

3.3 비정적 Inner Class

만약 비정적 Inner Class를 사용한다면 다음과 같은 형태가 될 것이다.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class CreateProductRequestDto {
    private String productName;

    private Integer price;

    private Integer stock;
    
    ...
    
    private List<ProductImageDto> productImageUrls;


    // 비정적 Inner Class로 선언
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @Builder
    public class ProductImageDto {
        private String productImageUrl;
        private boolean thumbnail;
        
        // 비정적 inner 클래스이므로 외부 클래스의 필드에 접근 가능
        public void displayProductDetails() {
            System.out.println("Product Name: " + productName);
            System.out.println("Brand Name: " + brandEngName);
        }
    }
}

 

 

비정적 Inner Class를 사용할 때는 외부 클래스 인스턴스가 필요하다.

CreateProductRequestDto의 인스턴스를 먼저 생성한 후, 이를 통해 ProductImageDto를 생성해야 한다.

public class Main {
    public static void main(String[] args) {
        // 외부 클래스 인스턴스 생성
        CreateProductRequestDto productRequest = new CreateProductRequestDto();

        // 비정적 Inner Class 인스턴스 생성 (외부 클래스 인스턴스 필요)
        CreateProductRequestDto.ProductImageDto productImage = productRequest.new ProductImageDto();
        
        ...
        
        // 외부 클래스의 필드에 접근하여 메소드 실행
        productImage.displayProductDetails();
    }
}

 

외부 클래스와 Inner Class가 강하게 연관되어 의존도가 높아지지만, 외부 클래스의 인스턴스 필드나 메서드에 자유롭게 접근할 수 있는 장점이 있다.

예를 들어, ProductImageDto에서 외부 클래스인 price, stock에 접근할 수 있는 것이다.

 

  • Inner class라고 하며 외부 인스턴스에 대한 참조가 유지된다.
  • 외부 인스턴스는 내부 클래스를 new를 통한 인스턴스 할당으로 멤버변수처럼 사용할 수 있다.
  • 외부에 대한 참조가 유지되므로 내부 클래스도 외부 클래스의 자원을 사용할 수 있다.

 

4. 더 알아보기

자료를 좀 더 조사해 보니, 정적 Inner Class를 권장한다는 사실을 알았다.

외부 클래스와의 불필요한 결합도를 줄일 수 있을 뿐만 아니라, 메모리 사용 효율성을 높일 수 있기 때문이었다.

 

비정적 클래스는 외부 클래스의 인스턴스에 대한 참조를 유지해야 하기 때문에, 외부 클래스 인스턴스가 메모리에서 해제되지 않는 문제가 생길 수 있고, 이에 따라 메모리 사용량이 불필요하게 증가할 수 있다.

 

인텔리제이에서도 비정적 클래스 형태로 작성하면 정적 클래스로 작성하라는 경고 문구가 나타난다.

 

반면, 정적 클래스는 외부 클래스 인스턴스에 대한 참조를 가지지 않으므로, 외부 클래스와는 독립적인 메모리에서 관리되기 때문에 더 효율적이라고 볼 수 있다.

 

 

5. 결론

하지만 Inner Class가 재사용될 여지가 많은 경우에는 별개의 Dto로 나누는 것이 좋을 것 같다.

이에 대해서는 dto 디렉토리 내부의 in, out으로 분류되기 애매하므로 dto/common라는 위치에서 관리하고자 한다.

728x90