1. "생각하는 코드가 아닌, 단순히 동작하는 코드"

처음 마주한 코드는 기능적으로는 동작할지 몰라도, 장기적인 관점에서 심각한 문제를 내포하고 있었습니다.
특히, 아래의 문제들은 소프트웨어의 유지보수성과 확장성을 크게 저해하는 요소였습니다.

// 기존 코드 - 심각한 문제점들
try {
    cnt = egovYeyakDAO.yeyakDelete(yeyakVO);
} catch (Exception e) {
    // 빈 catch 블록: 예외가 발생해도 아무런 처리 없이 넘어간다.
}

// 매직 넘버와 하드코딩
if("AP01".equals(category.gettState())) {
    TrainingListIdNm = "대기";
}

문제점 심층 분석:

  • catch 블록: 모든 예외를 포괄하는 Exception을 잡고 아무것도 하지 않는 것은 가장 위험한 안티패턴 중 하나입니다.
    이는 예상치 못한 오류의 원인이나 시점을 파악할 수 없게 만들어 디버깅을 불가능하게 합니다. 운영 환경에서 데이터가 유실되거나 정합성이 깨져도 시스템은 아무런 경고 없이 겉으로는 정상 동작하는 것처럼 보일 수 있습니다.
  • 매직 넘버와 하드코딩: AP01과 같은 **매직 넘버(Magic Number)**는 코드를 이해하기 어렵게 만듭니다.
    이 코드가 어떤 상태를 의미하는지 파악하려면 별도의 문서나 데이터베이스를 확인해야 합니다.
    이는 가독성을 크게 떨어뜨리고, 향후 상태 코드가 변경될 경우 모든 관련 코드를 일일이 찾아 수정해야 하는 위험을 초래합니다.

이러한 코드 구조는 단순히 기능 구현에만 급급했음을 보여주며, 안정적인 서비스 운영을 위한 기본적인 고려조차 이루어지지 않았다는 것을 의미합니다.


💡 구조적 문제 진단과 개선 방향 모색

2. "기능 구현만으로는 충분하지 않다. 시스템의 구조를 재정립해야 한다."

기존 코드는 단순히 리팩토링(Refactoring)만으로 해결될 수 있는 수준이 아니었습니다. 시스템 전반에 걸친 구조적 결함이 명확히 드러났기 때문에, 근본적인 설계 개편이 필요했습니다.

주요 구조적 문제:

  • 레이어 간 책임 혼재: 컨트롤러(Controller)가 사용자 요청 처리뿐만 아니라 비즈니스 로직, 데이터 접근 로직까지 모두 수행하고 있었습니다. 이는 **단일 책임 원칙(Single Responsibility Principle)**을 위반하며, 각 계층의 역할을 모호하게 만듭니다.
  • 계층 분리의 무의미: DAOService 계층이 존재했으나, 그 역할이 명확히 구분되지 않아 오히려 복잡성만 가중시켰습니다.
  • 일관성 없는 예외 처리: 어떤 곳에서는 예외를 무시하고, 다른 곳에서는 특정 반환값을 이용해 오류를 처리하는 등 일관성이 없어 유지보수 시 예측이 불가능했습니다.
  • 심각한 코드 중복: 유사한 로직이 여러 곳에 반복되어 나타났습니다.
    이는 변경이 필요할 때 여러 곳을 수정해야 하는 비효율성을 야기하고 오류 발생 가능성을 높입니다.

이러한 문제들을 해결하기 위해 도메인 주도 설계(Domain-Driven Design, DDD) 패턴을 적용하기로 결정했습니다.
DDD는 비즈니스 로직을 중심으로 코드를 설계함으로써 복잡한 비즈니스 도메인을 효과적으로 모델링하고 관리할 수 있게 해줍니다.


✅ 단계별 개선 과정 상세화

3. 1단계: 데이터 중심에서 도메인 중심 설계로 전환

기존 문제점: TrainingMasterVO와 같은 객체는 단순히 데이터를 담아 전달하는 VO(Value Object)에 불과했습니다. 이 객체 자체에는 어떤 비즈니스 로직도 담겨 있지 않았습니다.

개선 방향: 도메인 엔티티인 Training을 중심으로 설계하여, 비즈니스 규칙과 상태를 캡슐화했습니다.

// 개선: 도메인 중심의 엔티티
@Entity
public class Training {
    // 비즈니스 로직을 담은 메서드
    public boolean canAcceptApplication() {
        return !isFull() && schedule.isOpenForApplication();
    }
    
    public TrainingApplication createApplication(User user, ApplicationForm form) {
        // 도메인 규칙을 엔티티 내부에서 검증
        if (!canAcceptApplication()) {
            throw new TrainingNotAvailableException("Training is not available for application");
        }
        return new TrainingApplication(this, user, form);
    }
}

개선 의도:

  • 명확한 비즈니스 규칙: canAcceptApplication()과 같이 비즈니스 규칙을 메서드 이름으로 명확히 표현했습니다.
  • 캡슐화와 불변성: 관련된 필드들을 TrainingSchedule과 같은 **값 객체(Value Object)**로 묶어 관리하고, 불변성을 보장하여 데이터의 일관성을 유지했습니다.
  • 엔티티 내에서 비즈니스 로직 수행: Training 엔티티 스스로가 자신의 상태를 변경하거나 비즈니스 규칙을 검증하게 함으로써, **응집도(Cohesion)**를 높이고 **낮은 결합도(Low Coupling)**를 달성했습니다.

4. 2단계: 계층별 책임의 재정립

기존 문제점: Controller에 비즈니스 로직과 데이터 접근 로직이 혼재되어 있었습니다.

개선 방향:

  • Controller: HTTP 요청을 받아 유효성 검사 후 Service 계층으로 전달하고, 반환된 결과를 클라이언트에 응답하는 역할에만 집중했습니다.
  • Service: 여러 도메인 엔티티를 조율하고, 복잡한 비즈니스 로직을 처리하는 애플리케이션 서비스의 역할을 맡겼습니다. 트랜잭션 경계를 설정하고, 유효성 검사, 이벤트 발행 등을 담당하게 했습니다.
  • Repository: 데이터 영속성(Persistence)에 관련된 책임만 담당하게 하여 Service 계층이 데이터 접근 방식에 구애받지 않도록 했습니다.
// 개선: 비즈니스 로직은 서비스 계층에서
@Service
@Transactional
public class TrainingApplicationService {
    public ApplicationResult applyForTraining(...) {
        // 1. 비즈니스 규칙 검증: 중복 신청, 정원 초과 등
        // 2. 도메인 엔티티 조작: 신청 객체 생성, 상태 변경 등
        // 3. 영속화: Repository를 통해 데이터베이스에 저장
        // 4. 이벤트 발행: 후속 작업(알림 전송 등)을 위한 이벤트 발행
    }
}

개선 의도:

  • 관심사 분리(Separation of Concerns): 각 계층이 자신의 역할에만 집중하도록 하여 코드를 모듈화하고 이해하기 쉽게 만들었습니다.
  • 재사용성 및 테스트 용이성 향상: Service 계층의 비즈니스 로직은 Controller와 독립적으로 테스트할 수 있어 단위 테스트(Unit Test) 작성이 용이해졌습니다.
  • 느슨한 결합(Loose Coupling): 도메인 이벤트를 활용하여 신청 완료 후 메일 전송, 알림 발송 등 후속 작업을 비동기적으로 처리할 수 있게 함으로써, 시스템 간의 결합도를 낮추고 유연성을 확보했습니다.

5. 3단계: 강력하고 일관된 예외 처리 체계 구축

기존 문제점: 예외를 무시하거나, 반환값으로 오류를 처리하는 등 일관성이 없었습니다. 이는 오류 발생 시 시스템의 신뢰성을 저하시켰습니다.

개선 방향: 예외를 **비즈니스 예외(Business Exception)**와 **기술적 예외(Technical Exception)**로 구분하고, 스프링의 @ControllerAdvice를 이용한 전역 예외 핸들링을 도입했습니다.

// 커스텀 비즈니스 예외 정의
public class TrainingNotFoundException extends RuntimeException {
    // ...
}

// 전역 예외 처리
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(TrainingNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleTrainingNotFound(TrainingNotFoundException e) {
        // ...
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of("TRAINING_NOT_FOUND", e.getMessage()));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
        log.error("Unexpected error occurred", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of("INTERNAL_SERVER_ERROR", "An unexpected error occurred"));
    }
}

개선 의도:

  • 명확한 오류 메시지: TrainingNotFoundException과 같이 비즈니스 도메인에 특화된 예외를 정의하여, 어떤 문제가 발생했는지 명확하게 전달했습니다.
  • 일관된 응답 형식: 모든 오류를 ErrorResponse라는 통일된 형식으로 반환하여 클라이언트가 오류를 예측 가능하게 처리할 수 있게 했습니다.
  • 로깅과 모니터링: 예외 발생 시 로그를 남겨 운영 환경에서 문제를 신속하게 추적하고 해결할 수 있는 기반을 마련했습니다.

6. 4단계: 테스트 코드 작성의 습관화

기존 문제점: 테스트 코드가 전혀 없었기 때문에 리팩토링이나 기능 추가 시 기존 기능의 오작동 여부를 확인할 수 없어 불안했습니다.

개선 방향: **단위 테스트(Unit Test)**와 **통합 테스트(Integration Test)**를 병행하여 작성했습니다.

@ExtendWith(MockitoExtension.class)
class TrainingApplicationServiceTest {
    // @Mock, @InjectMocks를 사용해 외부 의존성을 격리
    @Mock private TrainingRepository trainingRepository;
    @InjectMocks private TrainingApplicationService service;

    @Test
    @DisplayName("정원이 남아있을 때 신청하면 승인 상태가 된다")
    void shouldApproveWhenCapacityAvailable() {
        // given, when, then 패턴 적용
        // ...
        // then
        assertThat(result.getApplication().getStatus()).isEqualTo(ApplicationStatus.APPROVED);
        // ...
    }
}

개선 의도:

  • 안정적인 리팩토링 기반 확보: 테스트 코드는 변경사항이 기존 기능에 영향을 주지 않는지 확인하는 회귀 테스트(Regression Test) 역할을 수행합니다.
  • 코드의 의도 문서화: 테스트 코드 자체가 비즈니스 로직의 명세서 역할을 합니다.
    예를 들어, "정원이 남아있을 때 신청하면 승인 상태가 된다"와 같은 테스트명은 해당 기능의 의도를 명확히 전달합니다.
  • 개발 생산성 향상: 새로운 기능을 추가할 때 예상치 못한 버그를 미리 발견하고, 디버깅 시간을 단축할 수 있습니다.

결론: 개발은 단순한 코딩이 아닌 소프트웨어 설계와 엔지니어링이다

이러한 과정을 통해 얻은 가장 큰 깨달음은 단순히 동작하는 코드를 만드는 것과 잘 설계된 소프트웨어를 만드는 것은 전혀 다르다는 것입니다.

  • 코드가 "왜" 그렇게 작성되었는지: 기술적 선택의 이유와 비즈니스 요구사항을 깊이 이해하는 것이 중요합니다.
  • 문제 해결 능력: 현재의 문제점을 정확하게 진단하고, 이를 해결하기 위한 최적의 기술적 방향을 제시하는 능력이 핵심입니다.
  • 협업의 중요성: 팀원들과 함께 코드 품질에 대한 공감대를 형성하고, 지속적인 코드 리뷰와 학습 문화를 만드는 것이 장기적인 성공을 위해 필수적입니다.

이제는 "이 코드는 어떤 비즈니스 규칙을 담고 있는가?",
"이 구조는 미래의 변화에 유연하게 대응할 수 있는가?"와
같은 질문을 끊임없이 던지며 개발에 임하고 있습니다.
이러한 관점의 전환이야말로 개발 역량을 한 단계 더 성장시키는 중요한 계기가 되었습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글