반응형
Spring에서 Util클래스를 작성할 때는 static이냐 Bean등록이냐를 고민하게 되는데 이번 글을 통해서 어떤 방식이 나을지 알아보도록 하자
1. 서론
1-1. 홍보
- 글의 시작부터 다른 글에 대한 홍보를 잠시 하겠다. 그 이유는 이번에 소개할 내용이 아래에서 소개하는 포스트에서 진행했던 내용과 관련이 있기 때문이다.
1-2. 작성하게 된 이유
- 간략하게 설명하면 SpringEvent리스너가 이벤트를 받아서 로직을 실행한다. 이 로직 안에서 Util클래스의 메서드를 가져다 사용하도록 했다. 이렇게 잘 동작하던 도중 우리가 코드 리뷰를 진행했는데 여기에서 갑자기 팀원 평양냉면7 님이 나한테 이렇게 물어봤다.
"이거 유틸 클래스는 대부분 static으로 바로 가져다 사용하지 않아?"
갑자기 머릿속에서 이전에 작성했던 코드들이 막 그려지면서 아! 항상 static을 사용했었지?라는 생각이 들었다. 이와 동시에 내가 유틸 클래스에 대해서 어떻게 사용하는 게 좋은 방법인지 모르고 있다는 것을 인지했고 이에 나는 어떻게 유틸글래스를 사용하는 것이 옳은 방법일지 고민해 보게 되었다.
말이 너무 길었던 것 같다. 이제 알아본 내용들을 설명하도록 하겠다
Start!
2. 내가 작성한 코드에 대한 설명
2-1. 유틸 클래스 설명
- 이번에 프로젝트를 하던 도중에 String값을 Json으로 변환해 줄 필요가 생겨서 전용 Util클래스를 생성했다. 이때 나는 클래스에 @Component를 적어줘서 이 유틸 클래스를 빈등록 해주는 방식을 택했는데 이 코드를 작성한 순간을 떠올려 보면 나는 Spring에서는 @Bean을 사용하는 것을 선호한다는 것을 알고 있었고 이것을 항상 지키고자 하다 보니 static에 대해서는 생각조차 하지 못했었다. (너무 하나만 바라보다 보니 생각이 짧아졌었던 것 같다.)
- 그리고 위에서 작성한 유틸 클래스 CustomJsonBuilder를 아래의 메서드 안에서 사용했다. 이때 유틸 클래스는 생성자 주입을 받기 위해 lombok이 지원하는 @RequiredArgsConstructor를 작성하고 필드에는 final로 선언해 두었다.
사실 이렇게 작성하고 코드를 실행하고 로직을 동작시켰을 때 전혀 문제가 없이 작동한다. 하지만 나는 빈 등록보다 static을 사용하는 방식을 왜 많이 택했는지 궁금했기에 지금부터 알아본 것을 공유하겠다.
3. Static 메서드 vs Spring Bean 어떤 것을 선택할까?
3-1. Static Util 클래스 사용 시점
- 종속성이 없고, 객체 생성 필요 없을 때
- Static 메서드는 추가적인 설정이나 의존성 주입이 필요 없이, 직접 클래스 이름으로 호출할 수 있어서 간편하다. 예를 들어, 유틸리티 함수들이 외부 상태나 서비스에 의존하지 않고, 자체적인 로직만 수행하는 경우에 적합하다.
- 일관된 결과 제공
- 입력 값이 같으면 항상 동일한 결과를 반환하는 함수에 적합하다. 이런 특성은 함수가 순수 함수(pure function)로 작동한다는 것을 의미한다. 예를 들어, Math.max, Collections.sort 등이 이에 해당한다. 이들은 입력에 대해 예측 가능한 결과를 제공하고 어떠한 외부 상태도 변경하지 않는다.
- 메모리 사용에 유리
- Static 메서드는 객체 인스턴스를 생성하지 않기 때문에 메모리 효율성이 높다. 특히 자주 사용되는 유틸리티 함수에 유용하다. 이는 애플리케이션의 전반적인 성능에 긍정적인 영향을 미칠 수 있다.
3-2. Spring Bean 등록 사용 시점
- 의존성이 있을 때
- Spring Bean은 의존성 주입을 통해 다른 컴포넌트와의 상호작용이나 상태 관리가 필요한 경우에 적합하다. 예를 들어, 데이터베이스 연결이나 외부 서비스와의 통신을 필요로 하는 유틸리티 클래스는 Bean으로 등록하는 것이 좋다.
- 테스트 용이성
- Bean으로 등록된 클래스는 Spring의 테스팅 프레임워크와 통합되어, Mocking이나 다른 테스팅 기법을 적용하기 쉽다. 특히, 복잡한 비즈니스 로직을 수행하는 클래스의 경우, 유닛 테스트를 통해 각 메서드의 동작을 독립적으로 검증할 수 있다.
- 결과의 일관성이 보장되지 않는 경우
- 예측할 수 없는 외부 요인에 의해 결과가 달라질 수 있는 경우, Bean으로 만들어 유연한 처리가 가능하다. 예를 들어, 네트워크 요청을 처리하거나, 외부 시스템의 상태에 따라 달라지는 로직을 포함하는 경우에 적합하다.
3-3. 그래서 나의 경우에는 둘 중 뭘 선택해야 하는가?
CustomJsonBuilder 클래스의 경우를 살펴보면, 이 클래스는 내부 상태를 관리하고 있고 (values 맵), 이 상태는 클래스의 메서드(add 및 build)에 의해 변경될 수 있는 상황이다.
3-3-1. Static 유틸 클래스로 만드는 경우 (적합 X)
- Static 유틸 클래스는 일반적으로 내부 상태를 유지하지 않아야 한다. 하지만 CustomJsonBuilder는 내부 상태(values 맵)를 유지하고 있다.
- Static 메서드는 입력에 대해 항상 동일한 결과를 반환하는 순수 함수로 동작하는 것이 이상적인데, 이 클래스는 내부 상태에 의존해 결과가 달라질 수 있다. 지금 내가 작성한 CustomJsonBuilder 클래스의 경우에는 내부 상태(values 맵)를 유지하고 있어서, 같은 입력에 대해 항상 동일한 결과를 반환하지 않을 수 있다.
예시를 들어보자
- 아래와 같이 코드를 작성해서 CustomJsonBuilder 유틸 클래스를 사용한다고 가정한다.
CustomJsonBuilder builder = new CustomJsonBuilder();
builder.add("key1", "value1");
String json1 = builder.build(); // {"key1": "value1"}
builder.add("key2", "value2");
String json2 = builder.build(); // {"key1": "value1", "key2": "value2"}
- 첫 번째 build() 호출
- builder 객체에 "key1": "value1"이 추가된다.
- build()를 호출하면, {"key1": "value1"}라는 JSON 문자열이 생성된다.
- 두 번째 build() 호출
- builder 객체에 추가적으로 "key2": "value2"가 추가된다.
- 이번에 build()를 호출하면, {"key1": "value1", "key2": "value2"}라는 JSON 문자열이 생성된다. 이전과 다른 결과가 나온다.
CustomJsonBuilder의 build() 메서드는 객체의 내부 상태(values 맵)에 따라 다른 결과를 생성할 수 있다. 이는 static 메서드가 순수 함수로 작동하며, 항상 동일한 입력에 대해 동일한 결과를 반환해야 한다는 원칙과 어긋난다. 따라서, 이 클래스는 static 유틸 클래스로 적합하지 않다. 내부 상태에 의존하는 메서드는 다른 결과를 반환할 수 있기 때문이다.
3-3-2. Spring Bean으로 등록하는 경우 (적합 O)
- 내부 상태를 관리하고 있는 클래스는 Spring Bean으로 등록하는 것이 적합하다. 이렇게 하면 객체의 생명주기를 Spring이 관리하고, 필요한 경우 의존성 주입을 통해 다른 Bean과의 상호작용을 쉽게 할 수 있다. 또한, Spring Bean으로 등록하면 테스팅이 용이해진다. Mock 객체나 Spring의 테스팅 프레임워크를 사용하여 유닛 테스트를 수행할 수 있다.
3-3-3. 결론
- CustomJsonBuilder 클래스의 경우, 내부 상태를 유지하고 관리하는 특성 때문에 Spring Bean으로 등록하는 것이 더 적합하다. 이 방식을 선택하면 더 유연한 구조를 가질 수 있고, 테스트 및 유지보수가 용이해진다.
위에서 설명한 말을 다시 읽어보니 조금 어려웠다.. 그래서인지 알 것 같으면서도 아직 뭔가 부족해서 조금 더 알아봤다.
4. 내가 만든 CustomJsonBuilder의 활용: 메서드 내 단일 사용과 Spring Bean 등록의 장점 설명
4-1. CustomJsonBuilder 사용의 특징 설명
- 메서드 내 단일 사용
- CustomJsonBuilder를 메서드 내에서 한 번만 사용하고 있다. 즉, 메서드 호출이 끝날 때마다 CustomJsonBuilder의 상태도 사라진다.
- 상태 공유 없음
- 각 메서드 호출에서 CustomJsonBuilder의 상태는 독립적임. 따라서 다른 메서드 호출에서 발생하는 상태 변화로 인한 부작용이 없다.
- DB에서 받은 데이터 처리
- DB에서 가져온 member 엔티티의 memberId를 JSON으로 변환할 때 사용되고 있다. 이 과정은 간단하고, 상태를 공유하거나 변경하는 복잡한 로직이 없다.
- DB에서 가져온 member 엔티티의 memberId를 JSON으로 변환할 때 사용되고 있다. 이 과정은 간단하고, 상태를 공유하거나 변경하는 복잡한 로직이 없다.
4-2. Spring Bean으로서의 이점
- 의존성 주입과 관리
- CustomJsonBuilder가 Spring Bean으로 등록되어 있기 때문에, Spring 프레임워크가 이 클래스의 인스턴스 생성, 관리 및 의존성 주입을 처리한다.
- 테스트 용이성
- Spring Bean으로서, 테스트 환경에서 이 클래스의 동작을 쉽게 모의(Mock)하거나 대체할 수 있다.
- 코드 일관성
- 프로젝트 전반에 걸쳐 일관된 방식으로 CustomJsonBuilder를 사용할 수 있다. 특히 Spring 프로젝트에서는 Bean으로 관리하는 것이 일반적이다.
결과적으로 CustomJsonBuilder를 Spring Bean으로 사용하는 것이 적합한 것 같다. 이 클래스를 메서드 내에서 단 한 번만 사용하고, 각 사용 사례가 독립적으로 상태를 관리하기 때문에, 상태 공유나 동시성 문제에 대해 걱정할 필요가 없다. 또한, Spring의 기능을 활용하여 의존성 관리 및 테스트를 쉽게 할 수 있는 이점이 있다. Static 유틸 클래스로 바꾸는 것보다 현재 방식을 유지하는 것이 좋을 것 같다.
5. 스프링 커뮤니티 Baeldung에서 제공된 정보
5-1. Singleton 패턴 vs Static 클래스
- Singleton 패턴은 애플리케이션 수명 동안 클래스의 단일 인스턴스를 보장한다. 이것은 전역 접근 포인트를 제공하는 반면, 인스턴스를 한 번만 생성한다는 것을 의미한다. 반면, Static 클래스는 객체 초기화가 필요 없다. 즉, 객체 생성 시간에 대한 오버헤드가 없으며, 컴파일 시간에 static 바인딩을 통해 더 효율적이고 빠르다는 장점이 있다.
5-2. Static 클래스 사용의 적합성
- Static 클래스는 입력 매개변수에만 작업을 수행하고 내부 상태를 수정하지 않는 다수의 static 유틸리티 메서드를 저장하는 데 적합하다. 런타임 다형성이나 객체 지향 솔루션이 필요하지 않은 경우에 특히 유용하다.
이 내용들을 종합해 보면 static 유틸 클래스는 상태를 변경하지 않고, 입력에 따라 일관된 결과를 반환하는 간단한 기능을 수행하는 데 이상적이라는 결론에 도달할 수 있다. 예를 들어, 문자열 처리나 수학 계산과 같은 단순하고 재사용 가능한 기능들이 이에 해당할 것이다. 반면, 유틸 클래스가 외부 자원이나 상태에 의존하거나, 런타임에 다양한 동작을 해야 하는 경우에는 Spring Bean으로 등록하는 것이 더 적합할 수 있다. 이렇게 되면, 의존성 주입, 테스트 용이성, 상태 관리 등의 이점을 활용할 수 있기 때문이다.
6. 최종 결론
6-1. 내가 생성한 유틸 클래스의 역할 정리
내가 작성한 CustomJsonBuilder 클래스에서는 객체의 상태를 내부에 유지하고 있다. 이 상태는 values라는 Map에 저장된 key-value 쌍을 의미한다. 이 클래스는 add 메서드를 통해서 이 Map 안에 값을 추가하고, build 메서드를 통해 JSON 문자열로 변환하는 역할을 한다.
6-2. Baeldung에 따르면 나는 어떤 방식을 적용시켜야 하는가?
Baeldung의 가이드라인에 따르면, static 클래스는 주로 상태를 내부에 저장하지 않고, 입력 매개변수에만 작업을 수행하는 유틸리티 메소드를 저장하는 데 적합하다고 한다. 즉, 나의 경우에는 CustomJsonBuilder 클래스는 내부 상태(values Map)를 가지고 있고, 이 상태는 객체의 메서드들에 의해 변경될 수 있는 상황이니 내가 작성한 유틸 클래스는 static 유틸리티 클래스로 작성하는 것은 적합하지 않아 보인다는 것이다. (나의 경우에는 스프링 빈을 등록해서 사용하는 게 더 좋은 방법인 것 같다.)
6-3. Spring Bean (@Component로 등록)으로 사용하는 경우의 장점
- 상태 관리: 객체의 상태를 유지하고 관리할 수 있다.
- 의존성 주입: 다른 Spring 컴포넌트와의 상호작용이 용이해지고, 테스트에 필요한 의존성을 주입하기 쉬워진다.
- 테스트 용이성: Mock 객체를 사용하여 테스트를 더 쉽게 수행할 수 있다.
이러한 조사한 내용들에 따라, 나는 내가 작성한 CustomJsonBuilder처럼 상태를 유지하고 관리해야 하는 경우에는 Spring Bean으로 유틸 클래스를 등록해서 사용하는 방식이 더 적합하다는 결론을 내렸다.
아래의 P6spy 설정 글을 읽어보는 것도 추천합니다!
2023.11.17 - [Spring Data JPA] - Spring Boot 3.x: P6Spy로 JPA 쿼리 로깅 마스터하기
이 포스트는 Team chillwave에서 사이드 프로젝트를 하던 중 적용했던 부분을 다시 공부하면서 기록한 것입니다.
시간이 괜찮다면 팀원 '평양냉면7' 님의 블로그도 한번 봐주세요 :)
반응형
'Spring MSA' 카테고리의 다른 글
SpringBoot MSA 로깅: Zipkin을 사용한 분산 추적에서 예외상황을 다루는 방법 (1) | 2023.11.22 |
---|---|
SpringBoot MSA 로깅: Zipkin으로 서버간 SNS, SQS, Feign 통신의 분산 로그 추적 하기 (1) | 2023.11.20 |
[Spring MSA] Zipkin으로 분산추적 로깅 구현하기 (0) | 2023.11.18 |
[Spring MSA] 스프링 이벤트와 SNS/SQS로 DB 정합성 보장 2탄 - ZeroPayload로 FeignClient 요청 (0) | 2023.11.17 |
[Spring MSA] Spring Event, SNS, SQS를 사용하여 DB 정합성 보장하기 1탄 (2) | 2023.11.15 |