프로필

데브고래밥

@devgoraebap

스택오버플로우의 단골손님이였던 Claude를 채찍질하는 개발자

Album Art

0:00 0:00
방문자 정보

요즘 관심있는

90
0
#java #spring

Spring Boot 개발기록 #4: 각 프레임워크들의 방법론과 관례

Spring에서 모든 상태는 private으로 관리하고, Lombok의 @Getter 어노테이션을 통해 접근을 제공하는 것까지는 좋았다. 하지만 @Setter를 다는 것은 내가 추구하는 개발 방법론이 아니기 때문에, 의도가 드러나는 정적 메서드(create, from 등)를 통해 객체 생성을 제공하려고 했다.

Spring은 생성자 오버로딩을 지원해서 확실히 TypeScript와 다르게 여러 상황에 맞는 객체 생성이 가능하다. 하지만 생성자만으로는 생성 의도를 명확히 표현하기 어렵기 때문에 보통 정적 메서드와 세트로 사용하게 되는데, 그렇게 되면 생성자도 여러 개 만들고 정적 메서드도 만들어야 해서 코드가 너무 장황해진다. 그래서 깔끔하게 Lombok 라이브러리를 통해 생성자를 코드에 노출하지 않고, 내부적으로 빌더만 사용하려고 요리조리 해보다가 문제가 생겼다.

TypeScript 습관이 만든 혼선

Lombok에 익숙하지 않다 보니 @AllArgsConstructor, @NoArgsConstructor, 그리고 @Builder 어노테이션을 조합하면서 애를 먹었다. 빌더를 외부에 노출시키지 않고 정적 메서드 내부에서만 사용하고 싶었는데, AI에 너무 의존하면서 공부하다 보니 맛탱이간 AI가 이상한 대답을 하는 것을 묵묵히 지켜보다가, 결국 Spring을 잘하는 팀 개발자에게 물어보고 너무 간단하게 해결했다.

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder(access = AccessLevel.PRIVATE) // 엑세스 레벨만 외부에서 접근할 수 없게 선언하면 됐음
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String content;
    private boolean completed;
    
    public static Todo create(String content) {
        return Todo.builder()
                .content(content)
                .completed(false)
                .build();
    }
}

그런데 한술 더 떠서, 빌더도 사용하지 않고 정적 메서드 안에서 일반 생성자로 인스턴스를 생성하고 프로퍼티 바인딩으로 과도한 빌더 사용 등을 말끔하게 해결해주셨다. 

@Entity
@Getter
@NoArgsConstructor
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String content;
    private boolean completed;
    
    public static Todo create(String content) {
        Todo todo = new Todo();
        todo.content = content;
        todo.completed = false;
        return todo;
    }
}

항상 readonly 키워드를 프로퍼티에 할당하여 프로퍼티를 불변으로 바꾸는 TypeScript 프로젝트를 진행하다 보니 인스턴스 생성 후 프로퍼티 바인딩을 할 수 있는 방법에 대해 잘 인지하지 못했다. 오늘 시간 중에 제일 유익한 시간이었다.

TypeScript 진영의 현실

이전에 TypeScript 메이저 프로젝트들은 어떻게 작성하는지 찾아봤었는데, 간만에 즐겨찾기 해놨던 예제 중 하나를 꺼내보자면, 대부분의 프로젝트에서 DTO와 Model 클래스에는 접근제한자를 사용하지 않는다.

오픈소스 Immich의 경우 여기서도 DTO 클래스에는 접근제한자를 과감하게 생략한 모습을 볼 수 있다. 

TypeScript의 private 키워드는 컴파일 타임에만 검사되고, 실제 JavaScript로 변환되면 일반 public 속성과 동일하다. 또한 DTO는 JSON 직렬화/역직렬화를 위해 plain object로 다뤄지는 경우가 많다. 그래서 typescript 커뮤니티를 보면 java 측과 확연히 차이나는 것 중 하나가 바로 모델 객체의 상태에 대한 캡슐화이다.

하지만 그렇다고 해서 아무런 제약 없이 프로퍼티를 노출하는 것이 옳다는 의미는 아니다. 오히려 개발 환경에서의 협업과 OOP 의도를 고려하면, 적절한 제약이 필요하다. 필자는 특정 프로퍼티를 제어하는 메서드를 만들어놨는데도 프로퍼티를 직접 수정할 수 있다면, 이는 설계 의도를 벗어나는 것이라고 생각한다.

나만의 해답

처음 OOP를 Spring으로 배운 뒤 오랫동안 Node.js 진영에서 일하다 보니, 이런 차이가 끝없는 고민거리가 되었다. "캡슐화를 위배하는 건 아닐까?", "OOP 원칙에 어긋나는 건 아닐까?" 같은 생각들 말이다.

프로그래밍 언어나 프레임워크는 각 진영에 맞는 특색 있는 방법들이나 관례가 존재한다. 무분별한 getter/setter가 진정한 캡슐화가 아닌 것처럼, 중요한 것은 객체의 상태를 제어하는 의도가 명확한 메서드를 노출하고, 값의 변경은 오로지 제공된 메서드를 통해서만 가능하게 하는 것이다.

따라서 나는 TypeScript 환경에서 DTO나 Model을 만들 때 다음과 같은 패턴을 사용한다:

  • private 접근제한자 대신 readonly 키워드를 사용하여 상태를 불변으로 만든다
  • 상태 변경이 필요한 경우, 정적 메서드나 인스턴스 메서드로 새로운 객체를 반환하는 패턴을 활용한다
  • (물론 내부적으로 값을 깔끔하게 생성해서 새로운 객체를 생성하기까지의 과정이 완벽하지는 않는다. 꼼수가 필요하다..)

Spring을 요즘 계속 사용해보니, 이곳은 Model, DTO 같은 객체에서 프로퍼티를 무조건적으로 은닉해도 Lombok 덕분에 개발 경험이 떨어지는 느낌을 받지 못했다. 다시 한번 언급하지만, 프로그래밍 언어나 프레임워크는 각 진영에 맞는 특색 있는 방법들이 존재한다. 같은 OOP를 지향하는 언어들이지만 디테일한 개발 경험은 확연히 다르다는 걸 새삼 느꼈고, 오늘 간만에 기분 좋은 개발을 한 것 같다.