오늘은 부스에 들어갈 카테고리(인생네컷, 하루필름, 포토매틱...)를 개발하려고 합니다!
초기 로직
CategoryConstants
처음에는 이 카테고리가 부스 내에 들어가야 하고, 단순히 String 형태로 넣어주면 되지 않을까 생각해서 아래와 같이 짜보았습니다
먼저, 카테고리의 상수값들을 별도 CategoryConstants 라는 클래스에 넣어둡니다.
public class CategoryConstants {
public static final String LIFEFOURUCUT = "lifefourcut";
public static final String SELFLEX = "selflex";
public static final String PHOTOMATIC = "photomatic";
public static final String HARUFILM = "harufilm";
}
이후, 아래와 같이 Booth 엔티티에서 String 형태의 카테고리를 불러오면 되지 않을까?? 라고 생각했습니다.
그리고 난 후, validateCategory로 데이터를 검증 하면 될 거라 생각했습니다.
@Getter
@Table(name = "booths")
@Entity
public class Booth {
...
@Column(name = "category", nullable = false)
private String category;
public Booth(String category) {
validateCategory(category);
this.category = category;
}
private void validateCategory(String category) {
if (!CategoryConstants.LIFEFOURUCUT.equals(category) &&
!CategoryConstants.SELFLEX.equals(category) &&
!CategoryConstants.PHOTOMATIC.equals(category) &&
!CategoryConstants.HARUFILM.equals(category)) {
throw new IllegalArgumentException("잘못된 카테고리 입니다.");
}
}
}
문제점
하지만 위 구조에서는 아래와 같은 문제점이 있습니다.
1. 확장성이 있을까??
: 향후에 카테고리가 많아지거나 변경해야할 때, CategoryConstants 와 validateCategory 메서드 둘 다 수정해야 합니다.
따라서, 확장성이나 유지보수성 측면에서 부족한 로직입니다.
2. 중복 코드 발생
만약 다른 class에서도 카테고리를 불러와서 사용해야 할 경우 validateCategory 메서드는 중복돼서 사용됩니다.
3. 가독성이 떨어진다
: 검증 로직이 향 후 길어질 경우 이는 불가피하게 위 1,2번 과정이 중복적으로 발생합니다.
개선 로직
enum 클래스
이런 상수 데이터들을 띄는 구조를 어떻게 개선시킬 수 있을까... 찾아보니 enum 클래스라는 키워드를 발견했습니다!!
enum 클래스를 간단하게 말하자면,
- 클래스처럼 보이게 하는 상수
- 서로 관련된 상수들끼리 모아 상수들을 정의하는 것
(참고로 enum 클래스는 열거체(enumeration type)으로 JDK 1.5 이상의 버전에서만 사용 가능합니다)
enum 클래스의 특징
> 열거형으로 선언된 순서에 따라 0부터 index 값을 가진다.
> enum 열거형으로 지정된 상수들은 모두 대문자로 선언한다.
> 열거형 변수들을 선언 후 마지막엔 세미클론을 찍지 않는다.
> 상수와 특정 값을 연결시킬 경우 마지막에 세미클론을 붙여줘야 한다.
enum 내 정의된 메서드
Static Methods |
valueOf(String arg) | String 값을 enum에서 가져온다. 값이 없으면 Exception 발생 |
valueOf(Class<T> class, String arg) | 넘겨받은 class에서 String을 찾아, enum에서 가져온다. valueOf(String arg)는 내부적으로 자기 자신의 class를 가져오는 것이다. | |
values() | enum의 요소들을 순서대로 enum 타입의 배열로 리턴한다. ENUM$VALUES의 카피이므로, 너무 자주 호출하는 것은 좋지 않음 |
|
Static 아닌 Methods |
name() | 호출된 값의 이름을 String으로 리턴한다. |
ordinal() | 해당 값이 enum에 정의된 순서를 리턴한다. (index 값 리턴) | |
compareTo(E o) | 이 enum과 지정된 객체의 순서를 비교한다. 지정된 객체보다 작은 경우 음의 정수, 동일하면 0, 크면 양의 정수를 반환한다. | |
equals(Object other) | 지정된 객체가 이 enum 정수와 같은 경우, true를 반환한다. |
enum 클래스 적용
@Getter
public enum Category {
LIFEFOURUCUT("lifefourcut"),
SELFLEX("selflex"),
PHOTOMATIC("photomatic"),
HARUFILM("harufilm");
private final String value;
Category(String value) {
this.value = value;
}
public static Category from(String value) {
for (Category category : values()) { // values()로 enum 타입의 배열 데이터를 받는다.
if (category.value.equals(value)) { // .value로 접근하여 배열 내 특정 데이터를 가져와 비교한다.
return category;
}
}
throw new BoothCategoryNotFoundException();
}
}
문제점 해결
위와 같은 구조를 변경함으로써 기존에 있던 문제점을 해결하였습니다.
1. 확장성이 있을까??
: 향후에 카테고리가 많아지거나 변경해야할 때에도, enum class로 정의된 Category만을 변경해주면 됨으로 확장성 측면에서 개선되었다!!!
2. 중복 코드 발생 제거
만약 다른 class에서도 카테고리를 불러와서 사용해야 할 경우 Category.from()을 통해 애초에 객체를 생성할 때, enum 클래스 내 데이터들을 모두 가져와 검증시킬 수 있다. 즉, Category.from()만 작성해놓음으로써 중복메서드를 작성할 필요를 줄일 수 있다!!
3. 가독성
: enum 클래스 내 데이터들을 가져와 비교함으로써 검증 로직의 변화가 없으므로 위 1,2번 과정이 중복적으로 발생하지 않는다!!
++4. 타입 안전성 보장
: 이는 예상하지 못했던 이점이었는데, 문자열 상수를 사용하면 잘못된 문자열값을 설정할 위험이 존재한다고 한다.
그런데 enum 클래스는 명확한 타입을 가지므로! 잘못된 값의 할당이나 사용을 컴파일 시점에서 예방시킬 수 있다!!
하지만 여전히 단점을 없을까요??
- 기존 String에 비해 더 많은 메모리를 사용하기에 서비스의 크기나 런타임 메모리 사용량의 증가가 된다..!
최종 : 람다 함수를 적용해볼 수는 없을까??
저번 시간에 학습한 람다 Stream을 for문을 대신해서 적용해보았습니다!
- Arrays.stream(values()) : Category enum class의 모든 값을 스트림으로 변환시킨다.
- .filter() : 내부의 각 데이터를 꺼내 비교 연산을 추가적으로 해준다.
- .findFirst() : 최종연산으로 findFirst 메서드를 활용해 Optional 형태로 반환받는다
- .orElseThrow( ::new) : 만약, findFirst() 메서드에서 Optional 형태의 데이터가 비어있다면 예외를 발생시킨다
public enum Category {
...
public static Category from(String value) {
return Arrays.stream(values())
.filter(bt -> bt.value.equals(value))
.findFirst()
.orElseThrow(BoothCategoryNotFoundException::new);
}
}
Category 데이터 테스트 진행
- assertThatThrownBy : 특정 메서드를 실행시킨 후의 데이터 검증을 처리하기 위해 작성해준다.
- isExactlyInstanceOf : 해당 메서드 과정 중 발생한 예외의 타입이 지정한 클래스 데이터와 완전히 일치했을 때를 검증한다.
- hashMessage : 발생한 에러의 message를 검증한다.
public class CategoryTest {
@Test
void 없는_카테고리를_찾을_경우_예외를_발생시킨다(){
String category = "flower";
assertThatThrownBy(()-> Category.from(category))
.isExactlyInstanceOf(BoothCategoryNotFoundException.class)
.hasMessage("존재하지 않는 카테고리입니다.");
}
}
'Project > 너도나도' 카테고리의 다른 글
[너도나도] EP 6. JWT를 도입한 이유 & 생성 (0) | 2023.08.28 |
---|---|
[너도나도] EP 4. Layered Architecture를 선택한 이유 (0) | 2023.08.15 |
[너도나도] EP 2. 도메인 & 엔티티 설계 (0) | 2023.08.13 |
[너도나도] EP 1. 개발 계획 (0) | 2023.08.11 |