본문 바로가기

우아한테크코스 7기

좋은 개발자란 뭘까?

완벽한 설계라는 무지개

 나는 우아한테크코스에서 공부하며 객체지향에 깊이 빠져있었다. 깔끔하게 모듈화되고, 책임이 명확하며, 재사용성이 높은 코드를 볼 때마다 예술 작품을 보는 듯한 희열을 느꼈다.

 

레벨 1, 2를 거치며 나만의 확고한 기준이 생겼다.

 

가장 완벽한 코드와 완벽한 설계를 적용하는 것은 아름다운 일이다.
모든 것은 추상화되어야 하며, 확장에 열려 있는 것이 옳다.

 

 

 나는 코드와 설계에는 정답이 있을 것이라는 강한 믿음을 가지고 있었다. 이 믿음은 나를 잡을 수 없는 무지개를 향해 달려나가게 만들었다.

어느 새, 더 클린하고  더 완벽하며, 더 아름다운 아키텍처를 찾아 헤매는 사람이 되어었다.

 

하지만, 프로젝트를 하면서 이 믿음은 깨지기 시작했다.

 완벽한 객체지향 설계와 아키텍처는 현실 세계에 존재하지 않는다는 것, 그리고 코드에는 맞다/틀리다는 이분법적인 개념 자체가 존재할 수 없다는 것을 깨달았다.

 

옳고 그름만 판단하는 과정에서 "모든 선택과 결정에는 항상 대가가 따른다는 진실"을 외면하고 있었다.

 

800줄의 복잡성과 5일간의 낭비

이 깨달음을 얻게 된 결정적인 계기는 스프링의 예외 핸들링 로직 리팩터링 시도였다.

 우리 팀은 하나의 GlobalExceptionHandler에 각 예외 별 @ExceptionHandler 메서드를 위치시켜, 한 클래스에서 모든 예외 처리 로직을 담당하고 있었다.

 

  GlobalExceptionHandler 하나에 모든 예외 처리 메서드가 모여있는 상황은 단일 책임 원칙을 어긴, 모듈화되지 못하고 유지보수성이 크게 떨어지는 불완전한 시스템으로 보였다.

 

또한, Spring의 예외처리 방식은 런타임 예외를 발생시킬 수 있는 가능성을 열어두고 있었다.

 개발자가 실수로 @ExceptionHandler 의 예외 클래스명과 @ExceptionHandler 메서드의 파라메터의 예외 타입을 일치시키지 않으면 런타임 예외가 발생한다.

 

// SRP 위반!!!
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ProblemDetail handleUnExpectedException(final Exception ex) {
        log.error("[UNEXPECTED] {}", ex.getMessage(), ex);
        ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
        problemDetail.setDetail(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
        return problemDetail;
    }

    @ExceptionHandler(BusinessException.class)
    public ProblemDetail handleRoutieException(final BusinessException ex) {
        log.warn("[EXPECTED] {}", ex.getMessage(), ex);
        return buildProblemDetail(ex.getErrorCode());
    }
    
    @ExceptionHandler(HandlerMethodValidationException.class)
    public ProblemDetail handle(final HandlerMethodValidationException ex) {
        log.warn("[EXPECTED] {}", ex.getMessage(), ex);
        return buildProblemDetail(ErrorCode.INVALID_REQUEST_DATA_VALUE);
    }
    
    // .....
}



// 개발자의 실수로 인한 런타임 예외 발생!!!
@ExceptionHandler(HandlerMethodValidationException.class)
public ProblemDetail handle(final IllegalArgumentException ex) {
    log.warn("[EXPECTED] {}", ex.getMessage(), ex);
    return buildProblemDetail(ErrorCode.INVALID_REQUEST_DATA_VALUE);
}

 

 이를 바로잡기 위해 예외처리 구조에 대한 대규모 리팩터링을 진행했다. 완벽하게 추상화되고 확장 가능한 시스템을 목표로 했다.

 그 결과, 새로 추가된 코드는 약 800줄에 달했다. 복잡한 설계를 적용하기 위해 제네릭 로직과 리플렉션 코드가 난무하기 시작했다.

 

 5일 동안 모든 구현을 마쳤고, 나는 뿌듯하게 PR을 올렸다. 팀원들이 내 설계를 전부 이해하고 긍정적인 피드백을 줄 것이라 기대했다.

 하지만 돌아온 반응은 코드를 이해하기 힘들고 시스템이 너무 복잡하다는 피드백뿐이었다. 약 5시간 동안 팀원들에게 내 코드를 설명하려 노력했지만, 결국 한 팀원의 질문이 나를 멈추게 했다.

 

 

"기존 방식도 잘 동작하는데 굳이 이렇게 복잡한 시스템을 구축해야 할 필요가 있을까요?"

 

 

 이 질문에 크게 반박할 수 없었다. 사실 기존 방식도 잘 동작하고 있었기 때문이다.

스스로에게 물었다. "나는 어떤 목적을 달성하기 위해 이렇게 복잡한 아키텍처를 구현하고 있는가?" "잘 동작하고 있는 시스템을 무엇을 위해 리팩터링하는가?"

 

 시스템이 구조적이고 아름다워지기 위해서, 많은 것들을 자동화하기 위해서는 필연적으로 많은 코드와 복잡한 설계가 필요하다는 아픈 진실을 마주했다.

 게다가, 예외처리 구조는 당장 급한 것이 아니었고, 추후 유지보수할 일이 크게 많지 않았기 때문에 5일이라는 시간을 "이미 잘 동작하는" 코드에 낭비한 것 자체가 큰 실수였다. 이 리소스는 더 중요한 비즈니스 로직, 즉 사용자를 위한 가치를 만드는 데 쓰일 수 있었다.

 

완벽해 보이던 설계가 무너진 순간 - 요구사항의 변경

문제는 여기서 끝나지 않았다.

"완벽해 보이는" 설계는 @ExceptionHandler 메서드가 매개변수로 Throwable만을 받는다는 가정 위에서 구축되었다.

 하지만 PR을 올린 후, 개인 공부를 통해 @ExceptionHandler 메서드가 HttpServletRequest, HttpServletResponse 등 다양한 매개변수를 받을 수 있다는 것을 알게 되었다.

 

요구사항이 확장된 것이다.

 

 내가 설계한 "완벽한 시스템"단 하나의 시나리오에서만 완벽했다. 요구사항이 확장되자 내 복잡한 설계는 순식간에 무너졌다.

 코드 몇 줄로 해결할 수 있던 문제가, "확장성, 유지보수성, 완벽한 추상화"를 지키려 했기 때문에 엄청난 양의 코드 변경과 설계 변경을 필요로 하는 문제로 변했다.

 실제로 이를 반영하는 데 500라인 정도를 다시 설계하고 구현해야 했다. 이 모든 것을 반영하고 나니, 설계는 걷잡을 수 없이 복잡해져 있었다.

 

역설적이게도, "완벽한 추상화"와 "확장성이 좋은 코드"가 오히려 문제를 더 복잡하게 만들고 있었다.

 

 결국, PR 작업 내용을 전부 롤백한 후 다시 구현하기로 결정했다. 이전까지의 복잡한 설계를 유지하기 매우 힘들 것이라 판단했기 때문이다.

 

 

좋은 개발자란 저울질을 할 줄 아는 개발자가 아닐까?

 이 경험을 통해 큰 깨달음을 얻었다. 완벽한 객체지향 설계와 클린한 코드, 아름다운 추상화와 설계는 현실에 존재할 수 없다. 소프트웨어는 그 자체로 아름답기 위해 존재하는 것이 아니라, 결국 사용자에게 가치를 전달하기 위해 존재한다.

 

내가 추구하던 가치는 순수한 개발적 미학이었을 뿐, 사용자에게 가치를 전달하는 목적을 가지고 있지 않았다.

 

 좋은 개발자란, 이상적인 좋은 코드"사용자에게 가치를 전달하는 것" 사이의 무게추를 현실에 맞게 능숙하게 조율할 줄 아는 사람이라는 철학을 가지게 되었다.

 

 코드가 완벽하지 않더라도 당장 비즈니스에 필요한 가치를 빠르게 전달하고, 꼭 필요한 곳에 적절한 수준의 추상화와 복잡성을 부여하는 현명한 실용주의자가 되는 것이 좋은 개발자의 모습이 아닐까?