객체지향 – Dreaming for the Future 영원한 개발자를 향해서. 월, 13 1월 2025 13:44:09 +0000 ko-KR hourly 1 https://wordpress.org/?v=4.7 108384747 좋은 코드에 대한 개인적인 생각 – 1 /index.php/2017/02/21/selfish-good-coding-1/ Mon, 20 Feb 2017 15:54:44 +0000 /?p=307

Continue reading ‘좋은 코드에 대한 개인적인 생각 – 1’ »]]> 사람들과 코드 리뷰를 하거나 면접을 보거나 하면서 다양한 코드를 접한다. 좋은 코드도 많이 봤다.  하지만 그보다 더 많은 나쁜 코드들도 봤다.  더구나 그런 코드들을 작성하는 분들이 경력 10년차 이상이라는 사실이 더 사람을 참담하게 만들었다.

경력이 비래해서 공통적으로 IT, 개발 사상에 대한 나름의 기준을 정립한 분들이다.  아마도 다른 곳에서는 본인이 다른 사람을 리딩하는 역할도 하고, 멘토링도 할 것이다.  하지만 코드가 그 모양인데 이 분들이 하는 리딩, 멘토링이 과연 맞는 것일까?  QCon 컨퍼런스에서 들은 말 가운데 “코드를 작성할 줄 모르는 아키텍트의 말은 사기다!” 라는 말이 정말 와 닿았다.

그러므로 코딩을 잘 해야한다.  하지만 어떻게 작성하는 코딩이 좋은 것인지 나름의 생각이 있을 것이다.

코드도 넓은 의미에서 글쓰기의 연장선이라고 생각한다.  그만큼 코드에는 작가(프로그래머)의 심미성이 반영될 수 있다.  그리고 기호에 따라 그 맛이 달라진다.  하지만 모든 글쓰기에 기본이 있듯이 코딩에서 기본이라는 것이 있다.  읽을 수 없는 글, 읽어도 뭔 이야기인지 알아들을 수 없는 글이 있는 것처럼 코딩에도 그런 쓰레기들이 존재한다.  쓰레기 코드라는 이야기를 듣지 않으려면 어떻게 해야할까?  글쓰기와 마찬가지로 코드를 읽고 작성해봐야 한다.

좋은 코드에 대한 예를 내 기준으로 기록해보고자 한다.  물론 이견이 있을 수 있고, 더 좋은 의견이 있을 수도 있다.  좋은 코딩에 대한 토론이 이어진다면 더할 나위 없을 것 같다.  하지만 “좋은 코드”에 대해 굳이 동의를 바리지 않는다.  다만 내가 기록해두고 앞으로 그 이상의 코딩을 할 수 있길 바랄 뿐이다.

몇 번까지 번호를 붙힐 수 있을지는 모르겠다.  생각나고 짬이 나는대로 써 볼 뿐이다.


먼저 간단히 다음의 코드를 살펴보자.

if (val < 13) { // for kids
  ...
} else if (13 <= val && val <= 18) { // for youth
  ...
} else { // for adult
  ...
}

잘못된 점을 탓하기전에 잘된점을 짚어보자.  얼핏보니 코드에 적절한 코멘트를 잘 사용한 것 같다?  확신이 들지는 않지만 그게 다인 것 같다.

그럼 잘못된 점들을 까보자!

  • 변수명이 개떡같다. 코멘트로 봐서 val 이라는 변수는 나이를 뜻한다. 그럼 당연히 변수 이름이 age가 되어야 한다.
  • 13, 18 이라는 숫자는 마찬가지로 나이를 뜻한다.  근데 뭘 의미하는 나이지? 의미를 알 수 없다.
  • if ~ else if ~ else 를 통해 하나의 코드 흐름에 3가지 분기를 치고 있다.

앞 선 두개의 지적은 초보적인 문제다.  바로 고칠 수 있다.

이 가운데 가장 잘못된 부분은 하나의 코드 흐름을 3개의 서로 다른 블럭으로 나뉜 부분이다.  다른 것들은 적절히 수정하면 금방 바로잡을 수 있지만 이 3가지 분기는 확실히 코드 읽기를 방해한다.  방해할 뿐만 아니라 이 코드를 그대로 방치하면 추가적으로 적용될 코드들도 마찬가지로 오염시킬 것이다.  그렇기 때문에 가장 큰 문제점이다.

디자인 패턴을 아는 사람이라면 제대로 고치는건 간단하다.

 

이 구조를 채택하면 위의 코드 구조를 아래와 같은 방식으로 변경할 수 있다.

...
Builder builder = new Builder();
ActionExecutor executor = builder.executor(age);
executor.execute();
...

앞에서 봤던 것과 같은 한 코드 영역에서 if .. else if .. else 와 같은 복잡 다단한 구조를 없앴다.  이에 대한 복잡도를 Builder -> Factory -> Executor 이어지는 연계 구조를 활용해서 깔끔하게 정리했다.  이런 방식이 주는 이점은 단순히 코드를 깔끔하게 만드는 것 이상의 의미가 있다.

  • 코드 읽기를 하나의 흐름으로 맞췄다.  읽는 과정에서 여기저기 눈을 움직일 필요없이 위에서 아래로 쭉 읽으면 된다.
  • 단위 테스트가 쉬워졌다.  이전 코드 구조에서 단위 테스트를 할려면 앞 전에 대한 조건등을 다 맞춰줘야 각 if .. else if .. else 사이 블럭에 대한 동작을 테스트할 수 있었다.  변경된 체계에서는 Executor 위주로 각 Executor가 정의된 동작을 하는지만 살펴보면 된다.  깔끔한 테스트를 만들어서 오류 자체를 효과적으로 제거할 수 있다.
  • 원래의 코드를 Mocking을 이용해 보다 다양한 상황에 대한 테스트 케이스를 보완할 수 있다.  실제 객체를 Injection하는 것보다는 Factory, Executor 인터페이스들을 Mocking으로 Injection하면 다양한 경우에 대한 테스트 케이스를 보다 쉽게 만들 수 있다.

혹자는 별것도 아닌 코드에 Factory니 Builder니 하는 계층을 이야기하는건 배보다 배꼽이 더 큰 이야기가 아니냐고 이야기할 지도 모른다.  하지만 한번 쓰고 버릴 코드가 아니라면 해야한다.  개인적으로는 한번 쓰고 버릴 코드도 해야한다고 주장한다.  코드를 작성하는 것도 습관이다.  그리고 잘못된 코딩 습관만큼 고치기 힘든 버릇도 없는 것 같다.

경우에 따라 빠르게 진행해야하는 코딩의 경우 Technical debt(s)를 감수하고 진행해야한다. 하지만 반드시 뒤따라 Refactoring이 이어져야한다.  하지만 한번 돌고 나면 만족하게 마련이다.  인간이라는게 이기적인 동물이기 때문이다. 목표를 달성하면 그 이후의 뒤정리는 그 이기심을 넘어서는 열정이 있어야 하는데 말이다. 그런 사람이 대부분이라고 생각하지 않는다.

따라서 대부분의 경우에도 이런 식으로 코딩해야한다.

– 끝 –

]]> 307
Springboot에서 Exception을 활용한 오류 처리 /index.php/2016/11/02/springboot-exception-handling/ Wed, 02 Nov 2016 06:44:03 +0000 /?p=254

Continue reading ‘Springboot에서 Exception을 활용한 오류 처리’ »]]> Java를 가지고 개발하는 오류 처리는 Exception을 활용하는 것이 정석이다.  개인적으로 값을 오류 체크하고 어떻게든 값을 만들어 반환하기보다는 오류가 발생하면 “오류다!” 라고 떳떳하게 선언하는 것이 좋은 방법이라고 생각한다.

RESTful API를 구현한 경우,  오류의 상태를 알려주는 가장 정석적인 방법은 HTTP Status Code를 활용하는 방법이다.  Exception을 통해서 이 응답 코드를 반환해주는 건 아주 쉽다.

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No candidate")
public class NotExistingCandidateException extends RuntimeException {
    public NotExistingCandidateException(String candidateEmail) {
        super(candidateEmail);
    }
}

이것과 관련해서 약간 말을 보태본다.  RESTful API를 개발하면서 응답 메시지의 Body에 상태 코드와 응답 메시지를 정의하는 경우를 왕왕본다.  RESTful 세상에서 이런 방식은 정말 안좋은 습관이다.   몇 가지 이유를 적어보면.

  1. 완전 서버 혹은 API를 만든 사람 중심적이다.  클라이언트에서 오류를 알기 위해서는 반드시 메시지를 까야한다.  호출한 쪽에서는 호출이 성공한 경우에만 처리하면 되는데 구태여 메시지를 까서 성공했는지 여부를 확인해야한다.
  2. API 클라이언트의 코드를 짜증나게 만들뿐만 아니라 일관성도 없다.  한 시스템을 만드는데 이런 자기중심적인 사람이 서넛되고, 상태 변수의 이름을 제각각 정의한다면 어떻게 되겠나?  헬이다.
  3. 표준을 따른다면 클라이언트 코드가 직관적이된다. Ajax 응답을 처리한다고 했을 때 성공은 success 루틴에서 구현하면 되고, 오류 처리는 error 구문에서 처리하면 된다.  프레임웍에서 지원해주기 때문에 코드를 작성하는 혹은 읽는 사람의 입장에서도 직관적이다.  더불어 성공/실패에 대한 또 다른 분기를 만들 필요도 없다.
  4. 이 경우이긴 하지만 Exception을 적극적으로 활용하는 좋은 습관을 가지게 된다.  코드를 작성하면서 굳이 Exception을 아끼시는 분들이 많다. 하지만 Exception은 오류 상황을 가장 명시적으로 설명해주는 좋은 도구이다.  문제 상황에서 코드의 실행을 중단시키고, 명확한 오류 복구 처리를 수행할 수 있는 일관성을 제공하기 때문에 코드의 품질을 높일 수 있다.

표준이 있으니 표준을 따르자.  이게 사려깊은 개발자의 태도다.

ResponseStatus라는 어노테이션을 사용하면 Exception이 발생했을 때, 어노테이션에 정의된 API Response Status Code로 반환된다.  이때 주의할 점은 정의한 Exception이 반드시 RuntimeException을 상속받아야 한다는 것이다.  Throwable을 상속받거나 implement 하는 경우에는 어노테이션에 의해 처리되지 않기 때문에 주의하자.

    @ExceptionHandler(UnknownAccessCodeException.class)
    public String handleUnknownAccessCodeException() {
        return "unknown";
    }

만약 Thymeleaf와 같은 UI framework을 사용해서 특정 오류 케이스가 발생했을 때 특정 뷰를 제공하고 싶다면, Controller 수준에서 ExceptionHandler 어노테이션을 활용할 수 있다.  위 예제는 Exception이 발생했을 때 unknown이라는 Thymeleaf의 뷰를 제공하라고 이야기한다.  이때도 주의할 점은 Exception은 반드시 RuntimeException을 상속해야한다는 점이다.

만약 어플리케이션 전반적으로 Exception에 대한 처리 로직을 부여하고자 한다면 ControllerAdvice 어노테이션을 활용한다.  어노테이션을 특정 클래스에 부여하면 해당 클래스에서 정의된 Exception 핸들러들이 전역으로 적용된다.

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

상세한 정보는 에서 참고하면 된다.

]]> 254
Amazing Lamda /index.php/2016/05/04/amazing-lamda/ Wed, 04 May 2016 06:10:12 +0000 /?p=120

Continue reading ‘Amazing Lamda’ »]]> Java 8에서 제대로 지원하는 stream과 람다(lamda)를 섞어서 쓰면 좋은데 람다가 코드를 어느 수준까지 줄여주는지를 한번 살펴보니 무시무시하다라는 생각이 든다.

public class OneLoginSimpleUserService {
    public UserAuthority processLogin(Authentication auth) {
        return new UserAuthority() {
            @Override
            public Collection<? extends Authority> getOwnedRoles() {
                return Arrays.asList(new Authority[]{ new Authority() {
                    @Override
                    public String toStringCode() {
                        return"ROLE_ADMIN";
                    }
                }});
            }
        };
    }
}

이걸 람다를 적용해서 고치면…

public class OneLoginSimpleUserService {
    public UserAuthority processLogin(Authentication auth) {
        return () -> Arrays.asList(new Authority[] { () -> "ROLE_ADMIN" });
    }
}

기존 자바의 인터페이스를 사용해서 불필요한 라인들이 딱 한줄의 return문으로 정리된다.

이걸 해석하는데 좀 시간이 필요하긴 한데, 얻을 수 있는게 참 많을 것 같다. 특히나 Interface를 활용해서 DI 기법으로 코드를 작성하는 방식이 더욱 더 각광받을 것 같다.

]]> 120
TDD를 하신다구요? /index.php/2016/03/16/omg-are-u-doing-in-tdd/ Tue, 15 Mar 2016 15:25:02 +0000 /?p=35

Continue reading ‘TDD를 하신다구요?’ »]]> 사람들과 전화너머로 이야기를 하다보면 TDD를 자신있게 말하는 분들을 종종 만난다.  물론 이 분들의 이력서에도 “활용 가능한 기술”들 가운데 하나로 TDD라는 3글짜 알파벳이 강렬하게 적혀있다. 개인적으로 TDD 방식의 개발의 예찬론자이기도 하기 때문에 이런 분들을 만날 때마다 반가운 생각이 든다.

처음 이 단어를 들었던 때가 아마도 2010년도 쯤이었을 것 같다. 주변의 개발자들 가운데 아는 사람도 적고 해서 손에 익히기에 쉽지 않았다. 지금은 거의 대부분의 이력서에 언급될만큼 보편적인 것이 되버렸다고 생각했다.

하지만 이런 분들을 만나서 “TDD 방식으로 코드를 작성해봐주세요~” 하면 다른 양상이 펼쳐진다. 테스트가 개발을 주도하는 모습을 기대했지만 대부분 테스트는 장식이다.  이 모습을 보면서 아래와 같은 생각을 해본다.

왜 이런 의미없는 테스트를 작성할까?

테스트를 작성하는게 테스트 주도 개발인가?

뭔가 착각이 있는 것 같다. 원래 테스트와 테스트 주도 개발은 다른 건데 말이다.

시작이 문제다.

2차원상의 좌표 점들의 거리를 계산하는 프로그램을 작성해야 한다고 치자. 뭐부터 작성해야할까?

public class DistanceCalculator {
  public void addPoints(int x, int y) {
  }
  public float calc() {
    return -1;
  }
};

테스트를 먼저 한다고 하지만 그래도 이정도는 먼저 찍어놓고 해야겠지?  본능적인 촉에 의하면 2차원 좌표점이라고 이야기를 했으니까 그건 x, y로 표시해야하고, 점들이 많을테니까 이걸 관리해야하는 기능도 필요할테니 점들을 넣을 수 있는 addPoint(x, y)와 같은 메소드도 필요하다.  그리고 이것들을 가지고 계산을 해야하니까 당연히 calc() 라는 메소드도 있어야 한다.  TDD로 개발하는 개발자니까 구현은 테스트를 작성한 다음에 하는거지!!!

이렇게 하는게 과연 Test Driven일까?  하지만 이 코드는 이미 Developed 되어있다.  해야할 일은 채워넣는 일일뿐.  여기에서 테스트가 하는 일은 이미 구조가 잡힌 코드의 안정성을 보장하는데 사용된다.  물론 예외등을 테스트하다보면 코드의 발전을 이끌 수는 있겠지만, 완전한 Driven이라고 이야기는 어렵다.

테스트 먼저

TDD를 좋아하는 개발자라면 시작은 DistanceCalculator 클래스가 아니라 DistanceCalculatorTest 클래스에 대한 코드부터 적어야 한다.

public class DistanceCalculatorTest {
  @Test
  public void shouldItCalculateDistance {
  final Point start;
  final Point end;
  final float expectedDistance = 1.0f;

  GIVEN: {
    start = new Point(0, 0);
    end = new Point(0, 1);
  }

  WHEN: {
    calculator = new Calculator();
    actualDistance = calculator.distance(new Point[] { start, end });
  }

  THEN: {
    assertThat(actualDistance, is(expectedDistance));
  }
};

이 테스트 코드를 통해 우리가 많은 것들을 정리할 수 있다.

  • 계산을 수행할 객체의 이름을 정했다. DistanceCalculator 보다는 Calculator가 현재의 컨텍스트에서 좀 더 좋겠다는 생각이 들었다.  (물론 테스트의 이름도 변경하는게 맞지만 예제를 위해…)
  • 계산을 수행하기 위한 메소드는 2차원 좌표를 기술할 수 있는 Point 객체의 배열을 받는다. addPoint등을 생각할 수 있지만, 그렇게 하면 테스트 코드를 작성하는게 번거로워진다. 테스트 자체를 통해 메소드의 사용성을 평가하고 쉽게 사용할 수 있는 방향으로 메소드를 설계한다.

여기까지를 정리했다면 이제 메인 코드를 작성할 때이다.  이클립스나 IntelliJ에서 제공해주는 Code Complete 기능을 사용하면 순식간에 찍어낸다.  위 두가지 과정에서 볼 수 있는 건 우리가 작성할 코드의 방향과 형태를 테스트를 작성해봄으로써 끌어냈다라는 것이다.  이것이 Test Driven의 진정한 모습이다.

이제 실패하는 테스트를 통해 로직을 완성하면 된다.

나누고 끄집어내라

자 우리의 말썽많은 고객분께서 요구 사항을 바꿨다.  2D 세상에 만족을 못하고 3D 세상에서도 거리를 계산해달라고 한다.  테스트를 다시 한번 살펴보자.

  WHEN: {
    ...
    actualDistance = calculator.distance(new Point[] { start, end });
    ...
  }

distance 메소드가 수행하는 역할(Responsibility)를 정리해보자.

  • 인접한 두 Point 간의 거리를 계산하고,
  • 계산된 결과값의 합을 구하는 역할을 한다.

2D, 3D일지의 문제는 첫번째 “인접한 두 포인트간의 거리”에만 영향을 미친다. 나머지 기능은 원래 distance 메소드가 수행하는대로 하면 된다. 두 점 사이의 거리 계산 부분을 수행하도록 Point 객체에게 위임해버리면 된다.

한방에 끝낼려고 하지 말자

인접 점들 사이에 거리를 어떤 방식으로 distance 메소드내에서 수행되어야 할지를 결정해야 한다. 이걸 어떻게 할지를 테스트를 통해 적절한 구조와 로직을 찾아보자.

public void Space2DPointTest {
  @Test
  public void should2DPointCalculateDistnaceWithOhter() {
    final Point p1;
    final Point p2;

    GIVEN: {
      p1 = new Space2DPoint(0, 0);
      p2 = new Space2DPoint(0, 1);
    }

    final float actual;
    THEN: {
      actual = p1.distance(p2);
    }

    final float expected = 1.0f;
    THEN: {
      assertThat(expected, is(actual));
  }
}

테스트 코드를 통해 포인트에 대한 구조를 아래와 같이 잡으면 된다는걸 알 수 있다.

  • Point라는 인터페이스를 둔다.
  • 인터페이스는 Point 객체를 파라미터로 받는 distance라는 메소드를 정의한다.
  • Space2DPoint 클래스는 Point 인터페이스를 구현한다.

비슷한 맥락에서 Space3DPoint에 대해서도 테스트 클래스를 작성해보자. 두 테스트에서 동일한 구조로 동작이 가능함을 확인한다.

확인이 완료됐다면 다시 원래의 CalculatorTest 클래스로 돌아가 이를 수정한다. 현재까지 작성된 테스트들의 관점에서 우리는 GIVEN 영역을 아래와 같이 수정하면 된다.

  GIVEN: {
    start = new Space2DPoint(0, 0);
    end = new Space2DPoint(0, 1);
  }

그리고 원래의 Calculator.distance() 메소드를 아래와 같이 변경한다.

public float distance(Point[] points) {
  ...
  for (int i=1; i&amp;lt;points.length; i++) {
    distanceSum += points[i-1].distance(points[i]);
  }
  ...
 return distanceSum;
}

흠… 이 과정이 리팩토링이다. 알흠답다. ^^;

끝날때까지 끝난게 아니다.

여러분이 만드는 서비스/제품이 계속 사용자들을 만난다면 여러분의 개발은 끝난게 아니다.

변화는 필수적이며 변화에 대응하기 위한 테스트와 이에 상응하는 리팩토링 역시 지속되어야 한다.  이 과정을 반복함으로써 여러분의 코드의 날이 날카롭게 빛날 것이다.

 

ps. 조금 더 긴 있다.  내용이 썩 맘에 들진 않지만 그래도 노력이 있으니까 한번 봐주길 바란다. 언젠가 기회가 된다면 이 글도 제대로 손을 봐야할 듯 싶긴 하다.

]]> 35