소개
개발 과정에서 유지보수성과 확장성을 고려하는 것은 매우 중요합니다. 특히, 시스템이 커지고 복잡해질수록 코드의 일관성과 가독성은 유지보수의 핵심 요소가 됩니다. 이번 글에서는 알림 코드 관리 문제를 Enum Converter를 활용해 해결한 사례를 소개합니다. 이 방법을 통해 알림 코드의 관리 효율성을 높이고, 유지보수의 편의성을 극대화할 수 있었습니다.
문제
프로젝트에서 알림 코드가 문자열(String) 타입으로 관리되고 있었습니다. 문제는 이 코드들이 구체적으로 어떤 알림을 발생시키는지 한눈에 파악하기 어렵다는 것이었습니다.
데이터베이스나 문서를 보지 않고서는 코드가 무엇을 의미하는지 알기 힘들었으며, 문서조차 최신 상태로 유지되지 않아 정확성을 보장할 수 없었습니다.
이러한 상황에서 알림 코드를 수정하거나 추가하는 작업이 매우 번거로웠습니다.
시간이 지나면서 어떤 코드가 무엇을 의미하는지 쉽게 잊어버리기 일쑤였고, 이로 인해 발생하는 오류를 방지하기 위해 주석이나 문서를 반복적으로 확인해야 했습니다.
문서나 주석이 코드와 별개로 관리되다 보니, 이는 오히려 데이터 일관성 문제를 유발할 가능성이 있었습니다.
해결
이 문제를 해결하기 위해 알림 코드 필드를 문자열(String) 타입에서 Enum 타입으로 변경했습니다. 이로 인해 코드의 의미를 명확하게 파악할 수 있게 되었고, 의도치 않은 오타나 잘못된 코드 사용을 방지할 수 있었습니다. Enum 자체가 일종의 문서 역할을 하도록 설계하여, 사용되지 않는 코드는 @Deprecated 어노테이션을 통해 명시적으로 표시했습니다.
아래의 코드들은 실제 프로젝트와 같은 코드는 아니나 유사한 형식으로 작성했습니다.
알림 엔티티 변경전
기존의 알림 엔티티는 다음과 같이 구성되어 있었습니다.
// 생략
public class Alarm extends Auditor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Comment("알림 ID")
@Column(name="alarm_id", columnDefinition = "bigint")
private long alarmId;
@Comment("알림 내용 코드")
@Column(name="alarm_code", columnDefinition = "varchar(20)")
private String alarmCode;
// 수신, 발신자 정보 등 필드 생략
}
이 코드만으로는 alarmCode 필드에 어떤 값이 들어가는지 알 수 없었습니다.
알림 발송 코드 변경전
알림 발송 코드는 다음과 같이 작성되어 있었습니다
// 다른 필드 정보는 생략
alarmService.save(Alarm.builder()
.alarmCode("AL10") // 배송중?
.build());
이 코드가 어떤 알림을 생성하는 것인지 주석으로밖에 알 수 없고, 이러한 코드가 곳곳에서 사용 중인데 주석이 안 적혀있는 곳도 많았습니다.
Enum을 이용한 리팩토링
기존에 운영 중인 데이터베이스에서 사용 중인 alarmCode 값을 모두 조회하여, 해당 코드를 Enum 필드로 변환했습니다. 이렇게 하면, IDE의 도움을 받아 사용하는 곳에서 어떤 코드가 어떤 알림을 발생시키는지, 현재 사용 중인 코드인지 아닌지를 쉽게 파악할 수 있습니다.
Enum 작성 예시
아래는 Enum으로 변환한 알림 코드의 예시입니다.
@Getter
@RequiredArgsConstructor
public enum AlarmCode {
AL1("등록됨"),
AL5("접수"),
AL6("진행중"),
AL7("요청"),
AL8("승인"),
AL9("제작중"),
AL10("배송중"),
AL11("배송완료"),
AL12("기간 만료"),
@Deprecated
AC2("새로운 글이 입력되었습니다."),
@Deprecated
AL3("파일이 업데이트 되었습니다."),
@Deprecated
AL4("승인되었습니다."),
;
private final String description;
}
이제 Enum을 통해 코드의 의미를 명확하게 알 수 있으며, 사용하지 않는 코드는 @Deprecated로 표시하여 관리할 수 있습니다.
jakarta.persistence.AttributeConverter 구현
Enum을 사용하더라도 운영 중인 과거 데이터를 위해 데이터베이스에는 여전히 문자열로 저장해야 합니다. 이를 위해 AttributeConverter를 구현하여 Enum 타입과 문자열 타입 간의 변환을 자동으로 처리했습니다.
@Converter(autoApply = true) // (1)
public class AlarmCodeConverter implements AttributeConverter<AlarmCode, String> {// (2)
// (3)
@Override
public String convertToDatabaseColumn(AlarmCode alarmCode) {
return alarmCode.name();
}
// (4)
@Override
public AlarmCode convertToEntityAttribute(String dbData) {
return AlarmCode.valueOf(dbData);
}
}
(1) @Converter(autoApply = true) 옵션
@Converter(autoApply = true) 옵션을 true로 설정하면, JPA는 이 컨버터를 모든 해당 타입의 속성에 자동으로 적용합니다.
기본값은 false이며, 이 경우에는 컨버터를 수동으로 지정해야 합니다.
만약 이 옵션을 사용하지 않거나 @Converter 어노테이션을 생략한 경우, 엔티티 필드에 @Convert(converter = AlarmCodeConverter.class)를 명시적으로 선언해야 합니다.
그렇지 않으면 아래와 같은 에러가 발생할 수 있습니다.
jakarta.persistence.PersistenceException: Converting `org.hibernate.HibernateException` to JPA `PersistenceException` : Unable to extract JDBC value for position `4`
(2) AttributeConverter 인터페이스
AttributeConverter 인터페이스는 제네릭 타입 두 가지를 정의합니다.
- X는 데이터베이스에서 읽어와서 사용할 타입, 즉 Enum 타입을 의미합니다.
- Y는 데이터베이스에 저장할 문자열 타입을 의미합니다.
아래는 AttributeConverter 인터페이스의 정의입니다.
package jakarta.persistence;
/**
* A class that implements this interface can be used to convert
* entity attribute state into database column representation
* and back again.
* Note that the X and Y types may be the same Java type.
*
* @param <X> the type of the entity attribute
* @param <Y> the type of the database column
*/
public interface AttributeConverter<X,Y> {
/**
* Converts the value stored in the entity attribute into the
* data representation to be stored in the database.
*
* @param attribute the entity attribute value to be converted
* @return the converted data to be stored in the database
* column
*/
public Y convertToDatabaseColumn (X attribute);
/**
* Converts the data stored in the database column into the
* value to be stored in the entity attribute.
* Note that it is the responsibility of the converter writer to
* specify the correct <code>dbData</code> type for the corresponding
* column for use by the JDBC driver: i.e., persistence providers are
* not expected to do such type conversion.
*
* @param dbData the data from the database column to be
* converted
* @return the converted value to be stored in the entity
* attribute
*/
public X convertToEntityAttribute (Y dbData);
}
(3) convertToDatabaseColumn 메서드
convertToDatabaseColumn 메서드는 엔티티의 Enum 필드를 데이터베이스에 저장할 문자열로 변환하는 역할을 합니다.
아래 예시는 간단한 구현 예시입니다.
// (3) null 값을 허용하는 경우 처리 해줘야함
@Override
public String convertToDatabaseColumn(AlarmCode alarmCode) {
if(alarmCode == null) { // null 일 경우 처리하는 코드 작성
return null; // 해당 코드는 db에 null로 저장한다는 뜻
}
return alarmCode.getDbValue(); // db 에 추가할 String 타입 value
}
이 메서드는 alarmCode가 null인 경우 null을 반환하여 데이터베이스에 null로 저장되도록 합니다.
만약 Enum 필드의 이름과 테이블에 저장되는 이름이 다를 경우, Enum 필드에 dbValue 같은 추가 필드를 정의하고, alarmCode.getDbValue()를 반환하도록 구현할 수 있습니다.
이 때 null 값을 허용하는 필드라면, 앞서 언급한 것처럼 null 처리를 해주지 않으면 NullPointerException(NPE)가 발생할 수 있으므로 주의해야 합니다.
(4) convertToEntityAttribute 메서드
convertToEntityAttribute 메서드는 데이터베이스에서 읽어온 문자열을 엔티티의 Enum 필드로 변환합니다.
// (4)
@Override
public AlarmCode convertToEntityAttribute(String dbData) {
return AlarmCode.valueOf(dbData);
}
이 예시에서는 자바의 Enum.valueOf() 메서드를 사용하여 문자열을 Enum으로 변환합니다.
주의할 점은 Enum.valueOf() 메서드는 대소문자를 구분하며, 주어진 이름과 일치하는 Enum 상수가 없을 경우 IllegalArgumentException이 발생할 수 있습니다.
따라서 이 메서드를 사용할 때는 데이터의 정확성을 사전에 검증하는 것이 중요합니다.
기존 코드에 반영하기
이제 AlarmCodeConverter를 구현했으므로, 이를 기존 코드에 반영하여 문자열로 관리되던 알림 코드를 Enum으로 변경할 수 있습니다. 이를 통해 코드의 가독성과 유지보수성을 높이고, 운영 중인 시스템에서도 안정적인 코드를 유지할 수 있게 됩니다.
알림 엔티티 변경후
기존의 알림 코드 필드를 문자열(String) 타입에서 Enum 타입으로 변경했습니다. 이를 통해 코드의 의미를 명확하게 하고, 실수로 잘못된 문자열을 입력하는 문제를 방지할 수 있습니다.
// 생략
public class Alarm extends Auditor {
@Comment("알림 내용 코드")
@Column(name="alarm_code", columnDefinition = "varchar(20)")
private AlarmCode alarmCode; // String -> Enum으로 변경
// 수신, 발신자 정보 등 필드 생략
}
이렇게 변경하면 코드상에서 alarmCode 필드가 AlarmCode Enum 타입으로 명확히 정의되므로, 코드의 가독성과 안전성이 크게 향상됩니다.
첫 번째 방법 - 엔티티에 @Convert 사용
첫 번째 방법은 엔티티 필드에 @Convert 어노테이션을 사용하여 특정 컨버터를 명시적으로 등록하는 것입니다.
이렇게 하면 JPA가 해당 필드에 대해 지정된 컨버터를 사용하도록 강제할 수 있습니다.
// 생략
public class Alarm extends Auditor {
@Convert(converter = AlarmCodeConverter.class) // 컨버터를 사용한다고 등록
@Column(name="alarm_code", columnDefinition = "varchar(20)")
private AlarmCode alarmCode; // String -> Enum으로 변경
// 수신, 발신자 정보 등 필드 생략
}
이 방법은 특정 엔티티 필드에만 컨버터를 적용하고자 할 때 유용합니다. 이 방법을 사용하면 해당 필드에 대해 명시적으로 컨버터를 적용할 수 있으며, 다른 필드에는 영향을 주지 않습니다.
두 번째 방법 - @Converter(autoApply = true) 사용
두 번째 방법은 컨버터 클래스에 @Converter(autoApply = true) 어노테이션을 추가하여 모든 해당 타입의 필드에 자동으로 컨버터를 적용하는 것입니다.
이렇게 하면 엔티티에서 별도로 @Convert 어노테이션을 사용할 필요가 없습니다.
@Converter(autoApply = true)
public class AlarmCodeConverter implements AttributeConverter<AlarmCode, String> {
@Override
public String convertToDatabaseColumn(AlarmCode alarmCode) {
return alarmCode.name();
}
@Override
public AlarmCode convertToEntityAttribute(String dbData) {
return AlarmCode.valueOf(dbData);
}
}
이 방법을 사용하면 프로젝트 내의 모든 AlarmCode 타입 필드에 대해 자동으로 컨버터가 적용되므로, 일관성 있게 Enum 변환을 처리할 수 있습니다.
모든 엔티티에 공통으로 컨버터를 적용하고 싶다면 이 방법이 더 효율적입니다.
두 방법 중 선택은 프로젝트의 요구사항에 따라 다릅니다. 모든 엔티티에 공통으로 컨버터를 적용하려면 두 번째 방법이 적합하며, 특정 엔티티 필드에만 적용하고 싶다면 첫 번째 방법을 사용하면 됩니다.
알림 발송 코드 변경 후
이제 알림 발송 코드에서 Enum을 직접 사용하여 코드를 보다 직관적이고 안전하게 작성할 수 있습니다.
// 다른 필드 정보는 생략
alarmService.save(Alarm.builder()
.alarmCode(AlarmCode.AL10) // 배송중 Enum으로 변경
.build());
Enum의 이름을 더욱 직관적으로 아래와 같이 변경하면 코드의 가독성을 높일 수 있습니다.
// 이런 코드가 더 좋음
alarmService.save(Alarm.builder()
.alarmCode(AlarmCode.IN_DELIVERY) // 내부를 안봐도 유추가능
.build());
이처럼 Enum 상수의 이름을 직관적으로 작성하고, 필요에 따라 dbValue를 추가로 구현하면 코드의 의미를 쉽게 파악할 수 있으며, 오류 발생 가능성을 줄일 수 있습니다.
예를 들어, AlarmCode.IN_DELIVERY와 같은 이름을 사용하면 코드만 봐도 해당 Enum이 어떤 의미를 가지는지 쉽게 알 수 있습니다.
Enum의 필드 활용
Enum 자체를 문서 역할로 활용하면, 코드 내부에서 어떤 알림 코드가 사용 중인지 쉽게 파악할 수 있습니다.
// 내부에 들어오면 바로 확인 가능 Enum 자체로 문서 역할
@Getter
@RequiredArgsConstructor
public enum AlarmCode {
AL1("등록됨"),
AL5("접수"),
AL6("진행중"),
AL7("요청"),
AL8("승인"),
AL9("제작중"),
AL10("배송중"),
AL11("배송완료"),
AL12("기간 만료"),
@Deprecated
AC2("새로운 글이 입력되었습니다."),
@Deprecated
AL3("파일이 업데이트 되었습니다."),
@Deprecated
AL4("승인되었습니다."),
;
private final String description;
}
이제 개발자는 더 이상 데이터베이스를 직접 보지 않고도 어떤 코드가 사용 중이며 어떤 의미를 가지는지 파악할 수 있습니다. 또한, 사용하지 않는 코드들은 @Deprecated 어노테이션을 통해 명확히 표시할 수 있습니다.
테스트 코드 작성
마지막으로, 엔티티의 필드를 String에서 Enum으로 변경한 후에는 컨버터가 올바르게 작동하는지 확인하는 테스트 코드를 작성하는 것이 좋습니다. 이를 통해 코드가 의도한 대로 작동하는지 확인할 수 있습니다.
@DataJpaTest
@EnableJpaAuditing
@Import(JPATestConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class AlarmRepositoryTest {
@Autowired
private AlarmRepository alarmRepository;
@DisplayName("알림 조회")
@Test
void 알림조회() {
// when
List<Alarm> testAll = alarmRepository.findTestAll();
// then
assertThat(testAll).isNotEmpty();
}
@DisplayName("알림 컨버터 작동 정상")
@Test
void 알림컨버터작동정상() {
// when
Alarm testAlarm = alarmRepository.findTestFirst();
// then
assertThat(testAlarm.getAlarmCode()).isNotNull();
assertThat(testAlarm.getAlarmCode().getClass()).isEqualTo(AlarmCode.class);
}
}
이 테스트 코드는 Alarm 엔티티가 데이터베이스와 올바르게 매핑되고, 컨버터가 정상적으로 작동하는지 확인하는 데 사용됩니다.
엔티티 필드에서 Enum 타입을 사용함으로써 빌드 시 발생하는 컴파일 오류를 사전에 잡아내고, 코드의 일관성을 유지할 수 있습니다.
마무리하며
기존에 문자열로 관리되던 알림 코드를 Enum으로 변경하면서 코드의 가독성과 유지보수성이 크게 향상되었습니다. Enum을 통해 알림 코드의 의미를 명확하게 하고, 오류 발생 가능성을 줄일 수 있었으며, 개발자는 더 이상 데이터베이스를 직접 보지 않고도 코드의 의미를 파악할 수 있게 되었습니다.
이를 통해 시스템의 유지보수성을 높일 수 있었습니다.