TIL/TIL

Inner Class로 DTO 관리

JoJobum 2023. 5. 10.

프로젝트 내 VO 혹은 DTO 패키지 안에 필요할 때마다 Class파일을 생성하면 DTO 파일들이 마구마구 늘어난다.

여기서 파생되는 문제점들은..

  • 부분적으로 중복되는 파일 갯수 자체가 많아지고 보기에 안좋다.
  • 더 이상 ClassName이 중복되지 않는 DTO를 만들기가 어려워집니다.
  • 필드들이 겹치는 DTO로 대충 Response를 내리다보니 Over-Fetching을 하게됩니다.

Inner Class로 DTO를 관리한다면 조금 더 깔끔한 패키지를 만들 수 있고, DTO ClassName을 정하는게 수월해진다.

그래서 Inner Class로 DTO를 관리하는 것이 좋지 않나? 라는 생각으로 이러한 방법을 검토해보았다.

 

예시 코드

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Data
    @NoArgsConstructor
    public static class Info {
        private int id;
        private String name;
        private int age;
    }

    @Data
    @NoArgsConstructor
    public static class Request {
        private String name;
        private int age;
    }
    
    @Data
    @NoArgsConstructor
    public static class Response {
        private Info info;
        private int returnCode;
        private String returnMessage;
    }
}

Inner Class에 왜 static을 붙이는가?

Nested Class

⇒ Non-static vs static

Non-static nested class(=Inner class) 은 같이 포함되어 있는 다른 class의 멤버들에 접근 가능(해당 class가 private여도)

static nested class는 다른 class의 멤버에 접근할 수 없다

Inner Class 는 인스턴스 메소드, 변수 처럼 inner class를 포함하는 클래스의 인스턴스에 연결되어 있기에 해당 객체의 메소드, 필드에 직접 접근할 수 있다.

즉 Inner class의 인스턴스는 outer class의 인스터스가 존재해야 존재할 수 있다는 것

반면 Static Nested Class 는 클래스 메소드, 변수처럼 행동하기에 outer class가 포함하는 다른 클래스에서 정의된 인스턴스 메소드, 변수에 직접 접근할 수 없다.

static nested class 는 다른 최상위 클래스들처럼 outer class 및 다른 클래스들의 인스턴스 member 와 상호작용을 한다.

 

💡 클래스 메소드와 인스턴스 메소드
클래스 메소드(static method): static 키워드를 가지는 메소드
인스턴스 메소드(instance method): static 키워드를 가지지 않는 메소드
클래스 메소드는 클래스 변수와 마찬가지로 인스턴스를 생성하지 않고도 바로 사용할 수 있습니다.
따라서 클래스 메소드는 메소드 내부에서 인스턴스 변수를 사용할 수 없습니다.

 

그래서 inner class에 왜 static 을 붙어야하냐고!! 라는 질문에 다시 답하자면

static 을 붙여주지 않으면 위 이미지와 같은 warning 이 표시된다.

  1. Inner class (Non-static nested class) 는 Outer class 의 인스턴스화 이후 Inner class 의 인스턴스화가 가능하며 두 인스턴스의 관계정보는 Inner class의 인스턴스 안에 만들어져 메모리 공간을 더 차지하며, 생성시간도 더 걸린다.
  2. 더 심각한 문제는 Inner class 가 Outer class 인스턴스에 대한 참조를 갖고 있기 때문에, Garbage Collection 은 Outer class 의 인스턴스를 수거 대상으로 보지 않아 GC 의 대상에서 빠지게 된다. (더 쉽게 풀어쓰자면 inner class, outer class 두 인스턴스가 연결되어 있어서 outer class 인스턴스의 메모리를 못 뺏는 것)

때문에 Outer class 를 참조할 일이 없다면, Nested class 는 static 을 붙여 무조건 static nested class 를 만들자

 

기존의 방식과 고민을 하게 된 배경

추가적인 고민에 대한 이야기를 하기 위해서는 애초에 이렇게 생각을 하게 된 배경이 필요할 것 갈다.

현재 개발하고 있는 시스템에서는 Map을 커스텀한 자료구조를 사용하여 데이터를 주고 받는 방식을 취하고 있는데,

장점

  • 주고 받을 때 클래스가 아닌 HashMap 기반의 key-value 이기에 데이터를 넣는 것에 대한 자유도가 좋다

단점

  • 데이터를 get/set 하는데에 어려움이 있음
  • 값들이 실제로 존재하는지 안하는지 검사가 안되기에 이에 대한 핸들링이 로직에 들어가야 했음
  • 유지보수가 어려움

이런 방식에서 현재 DTO 등을 사용하는 방식으로 전환하면서 고민이 생기게 된다.

우선 기존의 시스템을 보면 정말 많은 변수들이 Map의 형태 안에 들어가서 처리된다.

그리고 그 변수가 필수인 값들도 존재하고 선택적으로 들어있는 값들도 존재하기에 이에 대한 DTO를 구성하는데 어떻게 처리해야 할지에 대한 고민이 생겼다.

또한 각 메서드 마다 필요한 값들이 비슷하면서 다른 경우도 많은 상태이다.

(수많은 DTO들을 만들면서 든 생각은 HashMap이 사실 맞는 방법이 아니였을까? 라는 의문이 생겼을 정도로 현재의 방식은 비효율적이며 추후 유지보수가 어려운 방식이라 생각한다.)

 

💡 궁극적인 목표는 DTO, Entity 등의 데이터 처리를 위한 객체 재사용성을 높임으로서 유지보수가 편한 프로그램을 만드는 것이다.

 

이러한 관점에 서 DTO 클래스 파일 숫자를 줄이고 각 메소드에 종속시켜 Response, Request 등의 이름을가진 inner class로 DTO를 관리하면 더 줄지 않을까? 라는 생각을 하게 되었었다.

inner class로 DTO를 관리하면 DTO.Java 파일 자체는 줄 것이다.

하지만 Inner class로 한번 정리되어 우리가 가져다 쓰기 살짝 편해졌지만 DTO 자체는 재사용하지 못하고 필요할 때 마다 매번 새로 만들어야 하는 것을 동일하다는 생각이 들었다.

 

결론, 앞으로의 개선 방안

Inner class를 사용하기 보다는 

  • DB 테이블에 대한 이해도 필요
  • Entity와 Constructor를 활용하는 방법
  • 도메인, 비즈니스 관점에서 재설계

를 통해 Dto를 재사용할 수 있게 설계하는 것이 중요하다고 생각했다.

 

내가 이러한 문제점을 느낀 것은 이미 오래 운영되면서 잘못된 부분이 많아진 시스템을 이해도가 부족한 상태로 접하다 보니 수정없이 마이그레이션 하려고 했기 때문이라고 생각한다.

고로 도메인에 대한 이해를 바탕으로  Entity 와 Dto를 잘 쪼개면서 만들다 보면 우후죽순으로 생기는 Dto에 대한 문제를 해소할 수 있지 않을까?

 

 

=========================  추가적으로 알게 된 점  ==========================

inner static class가 Spring과 같은 멀티 스레딩 환경에서 가질 수 있는 위험성(미제)

 

heap 메모리는 객체 인스턴스가 할당되는 영역으로, new 연산자로 생성된 객체의 인스턴스가 저장되는 공간입니다. heap은 모든 스레드에서 공유되며, 객체가 생성되고 GC가 이를 수집하기 위해서는 해당 객체가 더 이상 참조되지 않는 상황이어야 합니다.

반면에 stack 메모리는 메서드 호출 시에 지역변수, 매개변수, 메서드 실행에 필요한 정보 등이 저장되는 공간으로, 각 스레드 별로 독립적으로 사용됩니다. 스레드마다 별도의 stack 영역을 가지기 때문에, 스레드 간의 메모리 공유 문제가 발생하지 않습니다.

따라서, static inner class 형태의 DTO 객체가 heap 메모리에 생성되면, 여러 스레드에서 동시에 해당 객체를 참조할 수 있기 때문에, thread-safe 하지 않을 수 있다는 생각이 들었다. 즉, heap 메모리에 생성된 객체가 수정될 수 있다면, 여러 스레드에서 동시에 해당 객체의 상태를 변경할 수 있기 때문에 thread-safe하지 않을 수 있다.

 

 

예를 들어 아래와 같이 DTO를 작성하고 사용한다고 했을 때

public class UserDto {
	public static class Request1 {
		int id;
		String name;
		@Builder
		private Request1(int id, String name){
			this.id = id;
			this.name = name;
		}
	} 
	public static class Request2 {
		private final int id;
		private final String name;
		@Builder
		private Request2(int id, String name){
			this.id = id;
			this.name = name;
		}
	}
}

우선 기본적으로 static inner class의 경우 outer 클래스의 인스턴스가 뜨지 않아도 inner 클래스의 인스턴스를 띄울 수 있음.

그래서 Builder 패턴을 통해 Request1의 인스턴스를 생성한다면, 롬복의 @Builder를 까보면 결국 public static class RequestBuilder 에서 Request 객체를 만들고 이를 반환하는 방식으로 되어 있는데 반환하고 RequestBuilder 객체는 GC에 반환된다고 함.

어쨌든 그래서 Request1의 객체가 생성되면 멤버 변수가 final이 아니기에 수정될 여지가 존재하는데, Heap 메모리에 새로운 객체가 생길것이고 Heap 메모리에 생성된 객체가 수정될 수 있다면 다른 스레드가 객체의 상태를 변경할 수 있으니 thread-safe 하지 않을 수 있겠다는 생각이 들었다.

만약 Request2처럼 멤버 변수들을 final로 사용한다면 thread-safe 해질 수 있지 않나 라는 생각인데 맞는 생각인지 잘 모르겠다.

 

이러한 고민을 하다보니 그렇다면 new 등을 통해 생성해서 사용하던 대부분의 객체들은 사실 thread-safe하지 않은 것이였나?? 그렇다면 멀티쓰레드 환경에서 트래픽이 높다면 문제가 없는 코드들은 도대체 어떻게 짜야하는 것인지? 라는 혼란에 빠지게 되었다.

반응형

댓글