문서 읽는 데 65분 · D2

D-2: 레거시 개선·코드 리뷰·아키텍처 맛보기 — 규칙을 외우는 게 주니어, 언제 깰지 아는 게 미들

목차 29
전체 11강 중 11강 · 클린코드
난이도 · 중급

ℹ️읽기 좋고 고치기 쉬운 코드를 쓰는 법 — 리팩토링·설계 원칙. 코드를 충분히 써본 뒤에 빛을 발하는 주제예요.

홍순구입니다. 드디어 이 과목의 마지막 시간이에요. 길게 달려왔습니다. 지난 시간엔 코드 스멜을 알아채고, 테스트로 안전망을 친 뒤 작은 걸음으로 리팩토링하는 한 바퀴를 돌았죠. 그런데 그때 우리가 고친 건 '내가 방금 짠 작은 예제'였어요. 오늘은 그 연습장을 덮고 현실로 나갑니다.

현실의 코드 리뷰에서 가장 자주 듣는 세 마디가 있어요. "레거시라 무서워서 손을 못 대요." "리뷰에서 뭘 봐야 할지 모르겠어요." 그리고 가장 깊은 한마디, "규칙은 아는데, 언제 깨야 할지를 모르겠어요." 오늘 이 셋을 차례로 풉니다. 테스트도 없고 의존성이 거미줄처럼 엉킨 레거시에 손댈 틈을 내는 법, 코드 리뷰에서 무엇을 보고 어떻게 말하는지, 그리고 코드를 담는 큰 그림인 아키텍처의 첫 스케치까지요.

그리고 마지막엔, 이 과목 내내 깔아 온 척추를 정면으로 세웁니다. 클린 코드는 도그마가 아니다. 규칙을 외워 모든 코드에 똑같이 들이대는 건 주니어예요. 그 규칙이 왜 생겼는지 알아 언제 적용하고 언제 깰지 판단하는 게 미들이고요. 좋은 코드를 보는 안목의 마지막 한 조각, 오늘 채웁니다.

텍스트
 오늘의 여정 — 원리에서 현실로, 그리고 안목으로

 ① 레거시 다루기    테스트 없는 코드에 이음새를 내고, 멈추지 않고 교체한다
 ② 코드 리뷰        무엇을 먼저 보고, 어떻게 건설적으로 말하나
 ③ 아키텍처 맛보기   계층형·의존성 규칙·헥사고날 — 큰 그림의 첫 스케치
 ④ 도그마 경계      "규칙을 외우는 게 주니어, 언제 깰지 아는 게 미들"

💡 오늘 수업의 핵심 — "좋은 코드의 안목은 규칙 암기가 아니라, 맥락을 읽고 언제 적용하고 언제 깰지 판단하는 힘이다"

🎯 학습 목표

  • 테스트 없는 레거시에 이음새(seam)를 내 의존성을 끊고, 안전하게 손댈 틈을 만든다.
  • 코드 리뷰에서 무엇을 먼저 보고 어떻게 건설적으로 피드백하는지 안다.
  • 계층형·헥사고날 아키텍처의 큰 그림을 잡고, 클린 코드가 도그마가 아님을 판단의 언어로 설명한다.

Step 1: "테스트 없는 코드가 레거시다"

먼저 '레거시'가 뭔지부터 짚어요. 보통은 '오래된 코드', '옛날 기술로 짠 코드'를 떠올리죠. 그런데 이 분야의 고전 『레거시 코드 활용 전략』을 쓴 마이클 페더스는 아주 도발적으로 정의합니다. "테스트 없는 코드가 레거시 코드다." 나이도, 언어도, 기술 스택도 상관없어요. 방금 짠 코드라도 테스트가 없으면 레거시고, 10년 된 코드라도 테스트가 단단하면 레거시가 아니라는 겁니다.

왜 이렇게 정의할까요? 코드를 고칠 때 가장 무서운 건 '고쳤더니 엉뚱한 데가 터지는' 상황이에요. 테스트가 있으면 고친 뒤 돌려 보고 "동작이 안 바뀌었다"를 바로 확인할 수 있죠. 테스트가 없으면 그 확인을 할 길이 없어요. 그래서 손을 못 대고, 못 대니 더 썩고, 더 썩으니 더 무서워지는 악순환에 빠집니다. 이게 "레거시라 무서워서 손을 못 대요"의 정체예요.

지난 시간에 우리는 이 두려움의 해법을 하나 배웠죠. 특성화 테스트로 현재 동작을 그물처럼 떠서 고정한 뒤 손대는 법이요. 그런데 현실의 레거시는 한 겹 더 고약합니다. 테스트를 짜려고 마음먹어도, 코드가 외부 세계에 너무 단단히 묶여 있어 테스트조차 짤 수 없는 경우가 많아요. 여기 그런 한 토막이 있습니다.

Java
// refactoring.legacy.OrderConfirmationBefore
public class OrderConfirmationBefore {

    public String confirm(String orderId, int amount) {
        EmailSender sender = new EmailSender();
        LocalDate today = LocalDate.now();
        String message = "주문 " + orderId + " (" + amount + "원) 확정 — " + today;
        sender.send(message);
        return message;
    }
}

겉보기엔 멀쩡해요. 주문 확정 메시지를 만들어 메일로 보내죠. 그런데 "이 메서드가 올바른 메시지를 만드나"를 테스트로 확인하려고 보면, 두 군데가 발목을 잡습니다.

첫째, new EmailSender(). 메서드 안에서 의존성을 직접 만들어 버려요. 그래서 테스트를 한 번 돌리기만 해도 진짜 메일이 나갑니다. 가짜로 바꿔 끼울 틈이 없어요. 둘째, LocalDate.now(). 실행하는 '오늘'에 묶여요. 같은 입력이라도 날마다 다른 메시지가 나오니, "2026-06-24가 나와야 한다"고 단언할 수가 없습니다. 테스트가 들어갈 입구 자체가 막혀 있는 거예요.

⚠️ 언제 깨나: 모든 레거시를 다 테스트로 덮으려 들지 마세요. 하루 돌고 버릴 일회성 스크립트, 곧 통째로 걷어낼 모듈에 안전망을 까는 건 과한 투자예요. 페더스의 조언도 "전부"가 아니라 "지금 손댈 코드 주변부터"입니다. 무서운 건 코드 전체가 아니라, 오늘 내가 고쳐야 하는 그 한 부분이거든요.

💡 한 줄 정리

레거시는 나이가 아니라 테스트의 유무로 정해진다. 그리고 진짜 무서운 레거시는 의존성이 외부에 단단히 묶여 테스트조차 짤 수 없는 코드다.

🙋 학생 질문 — "튜터님, 테스트가 없으면 레거시라면, 세상 코드 대부분이 레거시 아닌가요?"

날카로운 질문이에요. 그리고… 사실 맞아요. 페더스가 그 정의를 던진 이유가 바로 그겁니다. "레거시는 남이 짠 옛날 코드"라고 생각하면, 레거시는 늘 '남의 문제'로 멀어져요. 그런데 "테스트 없는 코드가 레거시"라고 정의를 당기면, 오늘 내가 짠 코드도 테스트가 없으면 레거시가 됩니다. 레거시가 갑자기 '내 문제'가 되는 거예요.

이게 불편하지만 건강한 시각이에요. 레거시를 만드는 건 시간이 아니라 우리 자신이라는 걸 인정하게 되거든요. 그래서 해법도 분명해져요. 새 코드엔 처음부터 테스트를 함께 짜서 레거시가 되지 않게 막고, 이미 테스트 없는 코드는 손댈 때마다 그 주변에 안전망을 조금씩 넓혀 가는 거죠. 오늘 배울 이음새 내기가 바로 그 '안전망을 넓히는' 첫 삽입니다.


Step 2: "이음새를 찾아 의존성을 끊는다"

그럼 테스트조차 못 짜는 Step 1의 코드를, 어떻게 테스트 가능하게 바꿀까요? 여기서 페더스의 또 다른 핵심 개념이 등장합니다. 이음새(seam)예요. 이음새란 "코드를 직접 고치지 않고도 동작을 바꿀 수 있는 지점"입니다. 옷의 솔기를 뜯어 다른 천을 덧대듯, 그 지점에서 구현을 갈아 끼우는 거죠.

Step 1의 코드는 이음새가 두 군데 막혀 있었어요. 의존성을 직접 만드는 new EmailSender()와, 시각에 묶이는 LocalDate.now(). 이 둘을 바깥에서 갈아 끼울 수 있게 열어 주면 됩니다. 먼저, 발송이라는 행위를 약속(인터페이스)으로 추상화해요.

Java
// refactoring.legacy.Notifier
public interface Notifier {

    void send(String message);
}

이게 이음새예요. 코어 로직은 "메시지를 보낸다"만 알면 되고, 그게 이메일인지 SMS인지 테스트용 가짜인지는 몰라도 됩니다. 이제 이 약속에 기대도록 코드를 고쳐요.

Java
// refactoring.legacy.OrderConfirmation
public class OrderConfirmation {

    private final Notifier notifier;

    public OrderConfirmation(Notifier notifier) {
        this.notifier = notifier;
    }

    public String confirm(String orderId, int amount, LocalDate today) {
        String message = "주문 " + orderId + " (" + amount + "원) 확정 — " + today;
        notifier.send(message);
        return message;
    }
}

두 이음새가 열렸어요. Notifier를 생성자로 주입받으니(객체 이음새), 테스트에선 진짜 발송 대신 가짜를 끼울 수 있고요. 날짜를 파라미터로 받으니(값 이음새), 고정된 날짜를 넘겨 결과를 단언할 수 있습니다. 눈여겨볼 건, 비즈니스 로직(메시지 형식·언제 보내나)은 한 글자도 안 바뀌었다는 거예요. 바꾼 건 오직 '의존성을 어디서 받느냐'뿐입니다.

이 "구체 클래스에 직접 매달리지 말고 약속에 기대라", 어디서 봤죠? 맞아요, B-2의 의존성 역전 원칙(DIP)입니다. 그때는 설계 원칙으로 배웠는데, 오늘은 그게 '레거시를 테스트 가능하게 만드는 실전 도구'로 돌아온 거예요. 이제 진짜로 테스트를 짤 수 있게 됐는지 볼까요. 발송을 기록만 하는 가짜 하나를 만들고요.

Java
// 테스트용 가짜 Notifier — 진짜로 보내는 대신 기록만 한다
class RecordingNotifier implements Notifier {

    private final List<String> sent = new ArrayList<>();

    @Override
    public void send(String message) {
        sent.add(message);
    }
    // sentCount(), lastMessage() 로 무엇을 몇 번 보냈는지 확인한다
}

이 가짜를 코어에 끼우면, 진짜 메일을 한 통도 보내지 않고 "무엇을, 몇 번 보냈는지"를 단언으로 확인할 수 있어요. Step 1에선 꿈도 못 꾸던 일이죠.

Java
@Test
void confirmSendsFormattedMessageThroughSeam() {
    RecordingNotifier notifier = new RecordingNotifier();
    OrderConfirmation confirmation = new OrderConfirmation(notifier);

    String message = confirmation.confirm("A100", 10000, LocalDate.of(2026, 6, 24));

    assertEquals("주문 A100 (10000원) 확정 — 2026-06-24", message);
    assertEquals(1, notifier.sentCount());
}

이음새를 연 덕분에, 고정된 날짜를 넘겨 메시지를 정확히 단언하고, 가짜가 발송을 한 번 받았는지까지 확인했어요. 안전망을 칠 수 있게 된 겁니다. 이제 이 코드는 마음 놓고 리팩토링할 수 있어요.

⚠️ 언제 깨나: 이음새를 무한정 내면, 모든 의존성이 인터페이스와 주입으로 뒤덮여 코드가 간접 참조의 미로가 됩니다. 정말 갈아 끼울 일이 없는 의존성까지 다 뽑아 추상화하는 건 과잉이에요. 이음새는 "테스트를 위해, 혹은 진짜 교체 가능성을 위해" 필요한 곳에만 냅니다. 처음부터 모든 걸 뽑지 말고, 손댈 곳 주변부터 하나씩.

💡 한 줄 정리

이음새는 코드를 고치지 않고 동작을 갈아 끼우는 지점이다. 하드코딩된 의존성을 주입·파라미터로 열면 테스트가 들어올 입구가 생기고, 그제야 안전망을 칠 수 있다.

🙋 학생 질문 — "튜터님, 이음새를 내려고 코드를 고치는 순간, 그게 테스트 없는 리팩토링 아닌가요? 닭이 먼저인지 달걀이 먼저인지…"

정확히 레거시 코드의 가장 아픈 역설을 짚으셨어요. "안전하게 고치려면 테스트가 필요한데, 테스트를 짜려면 먼저 코드를 고쳐 이음새를 내야 한다"는 딜레마죠.

페더스의 답은 이래요. 이음새를 내는 그 첫 변경만큼은, 위험을 극도로 낮춘 아주 작고 기계적인 수술로 한다는 겁니다. 메서드 안에서 new하던 걸 생성자 파라미터로 빼내는 것 같은 변경은, IDE의 자동 리팩토링 기능을 쓰면 사람 손이 거의 안 닿아 실수할 여지가 적어요. 동작을 바꾸는 게 아니라 '받는 위치'만 옮기는 거니까요. 그 최소한의 안전한 변경으로 입구를 연 뒤, 곧바로 테스트를 채워 안전망을 칩니다. 그다음부터의 본격적인 리팩토링은 그 그물 위에서 마음 놓고 하는 거고요. 첫 한 걸음만 숨죽여 떼면, 그 뒤로는 든든해집니다.


Step 3: "Strangler Fig — 멈추지 않고 교체한다"

이음새로 한 메서드를 구해 냈어요. 그런데 더 큰 질문이 남죠. 시스템 전체가 낡았다면요? "이 낡은 모듈을 새것으로 갈아엎고 싶다"는 마음이 들 때, 가장 위험한 선택이 빅뱅 재작성입니다. 운영을 멈추고 전부 새로 짜서 한 번에 교체하는 거죠. 지난 시간에도 봤지만, 재작성은 도박이에요. 새로 짜는 동안 비즈니스는 멈추고, 옛 코드에만 있던 수많은 예외 처리를 빠뜨리기 십상이거든요.

그래서 멈추지 않고 점진적으로 교체하는 전략이 있습니다. Strangler Fig(교살 무화과) 패턴이에요. 이름이 좀 섬뜩하죠? 열대의 무화과나무는 다른 나무를 타고 올라가 천천히 감싸고, 끝내 그 숙주 나무를 대체하며 자랍니다. 소프트웨어도 똑같이 해요. 낡은 시스템 곁에 새 구현을 조금씩 키우고, 트래픽을 한 갈래씩 옮기다가, 낡은 코드는 일거리가 말라 자연히 걷어내는 겁니다.

핵심 장치는 공통 약속(인터페이스)과, 그 뒤에서 트래픽을 나눠 주는 창구예요. 배송비 계산기를 예로 들게요. 낡은 계산기와 새 계산기가 같은 약속을 구현합니다.

Java
// refactoring.strangler.ShippingFee
public interface ShippingFee {

    int calculate(Parcel parcel);
}

그리고 창구 역할을 하는 라우터가, 어느 구현으로 보낼지 결정해요.

Java
// refactoring.strangler.ShippingFeeRouter
public class ShippingFeeRouter implements ShippingFee {

    private final ShippingFee legacy;
    private final ShippingFee modern;

    public ShippingFeeRouter(ShippingFee legacy, ShippingFee modern) {
        this.legacy = legacy;
        this.modern = modern;
    }

    @Override
    public int calculate(Parcel parcel) {
        if (parcel.domestic()) {
            return modern.calculate(parcel);
        }
        return legacy.calculate(parcel);
    }
}
텍스트
                 ┌─────────────────────┐
   배송비 요청 ─│  ShippingFeeRouter  │
                 └──────────┬──────────┘
                            │
                            ├─ 국내?  ─ NewShippingFee     (이전 완료)
                            └─ 국제?  ─ LegacyShippingFee  (아직 레거시)

   트래픽을 한 갈래씩 새 구현으로 옮기다, 낡은 코드는 일거리가 말라 걷어낸다

호출하는 쪽은 여전히 ShippingFee 하나만 봅니다. 그 뒤에서 라우터가 결정하죠. 국내 배송은 새 구현으로 이미 옮겼고, 국제 배송은 아직 낡은 구현이 처리해요. 국제까지 검증이 끝나면 라우터의 그 한 줄만 새 구현으로 바꾸면 됩니다.

여기서 절대 빠뜨리면 안 되는 게 있어요. 트래픽을 새 구현으로 옮기기 전에, 새 구현이 낡은 구현과 똑같은 결과를 낸다는 걸 먼저 증명해야 합니다. 어떻게요? 지난 시간의 특성화 테스트로요. 같은 입력에 두 구현이 같은 값을 내는지 확인한 뒤에야 안전하게 라우팅합니다.

Java
@Test
void domesticIsMigratedSafely() {
    Parcel parcel = new Parcel(1500, true);

    assertEquals(5000, legacy.calculate(parcel));
    assertEquals(5000, modern.calculate(parcel));
    assertEquals(5000, router.calculate(parcel));
}

낡은 구현도 5000, 새 구현도 5000, 라우터도 5000. 결과가 같으니 국내 배송을 새 구현으로 옮겨도 동작이 바뀌지 않아요. 이 '같음'을 확인하는 순간이, 트래픽을 옮겨도 좋다는 안전 신호입니다. 참고로 이 라우터는 C-2에서 본 퍼사드와 한 핏줄이에요. 복잡한 내부(어느 구현을 쓸지)를 단순한 창구 하나로 가린다는 점에서요.

⚠️ 언제 깨나: 작은 코드를 Strangler Fig로 교체하는 건 과합니다. 라우터를 두고, 옛 구현과 새 구현을 한동안 나란히 굴리는 '이중 운영'에는 그 자체로 복잡함과 비용이 따라요. 이 전략의 진가는 멈출 수 없는 큰 시스템, 한 번에 갈아엎기엔 위험이 너무 큰 레거시에서 나옵니다. 작은 모듈이라면 그냥 이음새 내고 테스트 친 뒤 한 번에 바꾸는 게 빠르고 깔끔해요.

💡 한 줄 정리

Strangler Fig는 공통 약속 뒤에서 트래픽을 한 갈래씩 새 구현으로 옮겨, 낡은 시스템을 멈추지 않고 교체한다. 단, 옮기기 전에 새 구현이 같은 결과를 낸다는 증명이 먼저다.

🙋 학생 질문 — "튜터님, 옛 코드와 새 코드를 동시에 굴리면, 같은 요청에 둘이 다른 답을 내는 사고가 안 나나요?"

바로 그 위험 때문에 실무에선 한 단계를 더 둬요. 트래픽을 새 구현으로 '완전히' 옮기기 전에, 한동안 두 구현을 같이 돌려 결과를 비교만 하는 기간을 갖습니다. 실제 요청은 여전히 낡은 구현의 답으로 응답하되, 똑같은 요청을 새 구현에도 몰래 흘려보내 두 답이 일치하는지 로그로 쌓는 거죠.

이렇게 한동안 지켜보다가, 두 답이 충분히 오래 일치하는 게 확인되면 그제야 응답을 새 구현으로 넘깁니다. 어긋나는 케이스가 잡히면 그걸 먼저 고치고요. 오늘 본 예제는 원리를 보여 주려고 라우터 한 줄로 단순화했지만, 실제 큰 교체에선 이 '한동안 같이 돌려 비교하기'가 안전벨트가 되어 줍니다.


Step 4: "코드 리뷰 — 무엇을 보나"

레거시를 다루는 손을 익혔으니, 이번엔 시선을 바꿔 봐요. 남의 코드를 들여다보는 일, 코드 리뷰예요. 처음 리뷰어가 되면 다들 막막해해요. "리뷰에서 뭘 봐야 할지 모르겠어요"가 그래서 나오죠. 화면 가득한 변경 코드를 앞에 두고 무엇부터 봐야 할지 모르면, 결국 눈에 띄는 사소한 것(들여쓰기, 변수명 취향)만 잔뜩 지적하게 됩니다. 정작 중요한 걸 놓치고요.

리뷰에는 우선순위가 있어요. 위에서부터 봅니다.

텍스트
 코드 리뷰에서 무엇을 먼저 보나 — 위가 더 중요하다

 ① 정확성    이 코드가 진짜 의도대로 동작하나? 빠뜨린 경우(엣지)는?
 ② 설계      책임이 잘 나뉘었나? 결합도는? 이 변경이 옆을 깨뜨리나?
 ③ 가독성    6개월 뒤 남이 읽고 이해할 수 있나? 이름은 의도를 드러내나?
 ④ 스타일    들여쓰기·줄바꿈·따옴표 — 가장 덜 중요하고, 대개 도구가 처리

정확성이 맨 위예요. 아무리 깨끗해도 틀린 코드는 의미가 없으니까요. 그다음이 설계, 그다음이 가독성, 맨 아래가 스타일입니다. 초보 리뷰어가 가장 많이 하는 실수가 이 순서를 거꾸로 밟는 거예요. 스타일부터 잡느라 본질을 놓치는 거죠.

그런데 ②와 ③, 그러니까 설계와 가독성을 볼 때 무엇을 보면 되냐고요? 사실 우리는 이미 그 체크리스트를 다 배웠어요. 이 과목 전체가 코드 리뷰의 렌즈였던 겁니다.

무엇을 보나 어디서 배웠나 리뷰에서 던지는 말
이름이 의도를 드러내나 A-1 "이 변수명만 봐선 뭔지 모르겠어요"
함수가 너무 길거나 여러 일을 하나 A-2 "이 함수, 한 문장으로 설명이 안 돼요"
객체가 속을 다 까발리나 A-3 "getter로 내부를 그대로 노출했네요"
null·예외 처리가 위태로운가 A-4 "여기 null이 새면 터질 것 같아요"
한 클래스가 바뀔 이유가 여럿인가 B "이 클래스, 책임이 두세 개 섞였어요"
패턴을 과하게 썼나 C "이건 함수 하나면 될 걸 클래스 다섯 개로…"
코드 스멜이 풍기나 D-1 "중복이 세 군데인데 묶을 수 있겠는데요"

보세요. A부터 D까지, 우리가 배운 모든 게 리뷰어의 눈이 됩니다. 리뷰란 결국 "내가 아는 좋은 코드의 기준을, 남의 코드에 비춰 보는 일"이에요. 그래서 이 과목을 끝까지 들은 여러분은, 이미 꽤 좋은 리뷰어의 렌즈를 갖춘 셈입니다.

⚠️ 언제 깨나: 체크리스트를 기계적으로 전부 들이대면, 사소한 지적이 폭탄처럼 쏟아져 작성자가 질려 버립니다. 맥락을 보세요. 마감이 코앞인 핫픽스라면 정확성만 보고 나머진 다음으로 미룰 수 있고, 주니어의 첫 PR이라면 한 번에 다섯 개만 골라 짚는 게 낫습니다. 리뷰는 흠집 내기 대회가 아니라, 함께 코드를 낫게 만드는 협업이에요.

💡 한 줄 정리

코드 리뷰는 정확성 → 설계 → 가독성 → 스타일 순으로 본다. 그리고 설계·가독성의 체크리스트는, 이 과목에서 배운 A·B·C·D 전부다.

🙋 학생 질문 — "튜터님, 리뷰하다가 '이건 내 취향이고 틀린 건 아닌데' 싶을 때가 많아요. 그것도 지적해야 하나요?"

아주 중요한 감각이에요. '틀린 것'과 '내 취향과 다른 것'을 구분하는 게 좋은 리뷰어의 핵심 자질이거든요. 변수명을 count로 했는데 나라면 total이라 했을 것 같다 — 이건 대개 취향이지 틀림이 아니에요. 이런 걸 강하게 밀어붙이면 작성자는 "이 사람은 트집을 잡는다"고 느끼고, 정작 중요한 지적까지 흘려듣게 됩니다.

그래서 권하는 방식이 있어요. 취향 차이는 아예 말하지 않거나, 말하더라도 "이건 제 취향인데요"라고 명확히 꼬리표를 달아 '꼭 안 고쳐도 된다'는 신호를 주는 거예요. 영어권 리뷰 문화엔 'nit'(사소한 것)이라는 표기가 있어서, "nit: 이 이름 이게 더 읽기 좋을 것 같아요"처럼 가볍게 던지고 결정은 작성자에게 맡깁니다. 리뷰어의 무게는 '정확성과 설계'에 실어야지, 취향에 실으면 안 돼요. 다음 Step에서 이 '어떻게 말하나'를 더 파 봅시다.


Step 5: "건설적 피드백, 그리고 취향은 도구에 맡긴다"

무엇을 보는지 알았으니, 이제 어떻게 말하나입니다. 사실 이게 더 어려워요. 같은 지적도 어떻게 전하느냐에 따라 약이 되기도 하고 독이 되기도 하거든요. 코드 리뷰는 사람과 사람 사이의 일이라, 기술만큼이나 말투가 중요합니다. 같은 내용을 담은 두 코멘트를 비교해 볼게요.

텍스트
 ❌ 나쁜 코멘트                          ✅ 좋은 코멘트

 "이거 왜 이렇게 짰어요?"              "이 변수명이 의도를 잘 안 드러내는 것
 "여기 완전 틀렸는데요."                같아요. activeMembers는 어떨까요?"
 "이 정도는 기본 아닌가요?"            "여기 입력이 비었을 때 NPE가 날 수 있을
                                       것 같은데, 한번 확인해 주실래요?"

왼쪽과 오른쪽의 차이가 보이세요? 몇 가지 원칙으로 정리됩니다.

첫째, 사람이 아니라 코드를 향해 말해요. "당신은 왜"가 아니라 "이 코드는"으로 시작합니다. 둘째, 명령보다 질문과 제안으로요. "고치세요"가 아니라 "이건 어떨까요?"가 상대의 방어를 풉니다. 셋째, 근거를 함께 줍니다. "틀렸어요"가 아니라 "비었을 때 NPE가 날 수 있어서요"처럼 왜를 붙여요. 넷째, 좋은 점도 짚어 주고요. 리뷰가 지적만 가득하면 받는 사람이 위축됩니다.

그런데 리뷰에서 가장 소모적인 싸움이 뭔지 아세요? 들여쓰기를 탭으로 할지 스페이스로 할지, 세미콜론을 붙일지 말지, 따옴표를 작은걸로 할지 큰걸로 할지 — 이런 취향 논쟁이에요. 정답이 없는데 사람마다 고집이 있어서, 여기에 리뷰 시간을 다 쓰면 정작 정확성과 설계는 못 봅니다.

해법은 간단해요. 이런 취향은 사람이 싸우지 말고 도구에 맡깁니다. A-1에서 자동 포매터를 잠깐 봤죠. 코드 스타일을 자동으로 통일해 주는 도구요. 거기에 더해 린터(linter)라는 도구가 있어요. 코드에서 흔한 실수 패턴(쓰지 않는 변수, 위험한 비교 등)을 자동으로 잡아 줍니다. 포매터와 린터를 팀의 자동 검사에 걸어 두면, 스타일과 단순 실수는 사람이 보기 전에 도구가 처리해요. 그럼 사람은 도구가 못 보는 것, 즉 로직과 설계에만 집중할 수 있습니다. 이게 리뷰를 건강하게 유지하는 핵심이에요.

⚠️ 언제 깨나: 정중한 질문형이 늘 옳은 건 아니에요. 한밤중 장애가 터져 핫픽스를 다투는 순간엔, 부드러운 돌려 말하기가 오히려 사치입니다. "여기 이 조건이 반대예요, 지금 바로 고쳐야 해요"처럼 짧고 명확한 게 나아요. 또 모든 지적을 칭찬으로 감싸려다 핵심 문제가 흐려지면 안 됩니다. 부드러움은 수단이지, 명확함을 희생하면서까지 지킬 목적이 아니에요.

💡 한 줄 정리

건설적 피드백은 사람이 아니라 코드를 향해, 질문과 근거로 말한다. 그리고 취향 논쟁(스타일)은 사람이 싸우지 말고 린터·포매터에 맡겨, 리뷰는 로직과 설계에 집중한다.

🙋 학생 질문 — "튜터님, 제가 리뷰받는 입장일 때 지적이 많으면 솔직히 좀 상처받아요. 이건 어떻게 받아들여야 하죠?"

그 마음, 누구나 겪어요. 그리고 그걸 넘어서는 게 주니어에서 미들로 가는 또 하나의 관문이에요. 핵심은 코드와 나를 분리하는 것입니다. 리뷰어가 지적하는 건 '내 코드의 이 부분'이지 '나라는 사람'이 아니에요. 코드는 고치면 그만이고, 그 지적 하나하나가 사실은 공짜 과외예요. 시니어가 내 코드를 들여다보며 더 나은 길을 알려 주는 거니까요.

그래서 시각을 뒤집으면 편해져요. 지적이 많다는 건 그만큼 배울 게 많다는 뜻이고, 그걸 흡수한 다음 PR은 분명히 더 좋아집니다. 반대로 리뷰어가 아무 말도 안 하고 통과시키는 게 오히려 무서운 거예요. 성장의 기회가 사라지는 거니까요. 한 가지 덧붙이면, 정말로 무례한 리뷰(사람을 공격하는)는 받아들일 필요 없어요. 그건 리뷰 문화의 문제지 여러분의 문제가 아닙니다. 좋은 지적은 흡수하고, 무례함은 거르세요.


Step 6: "아키텍처 맛보기 ① 계층형과 의존성 규칙"

지금까지 우리는 코드 한 줄(A), 클래스 하나(B), 패턴 하나(C), 리팩토링 한 걸음(D)을 봤어요. 이제 한 발 더 물러나, 코드를 담는 큰 그릇을 봅니다. 아키텍처예요. 파일이 수백 개로 불어나면 "이 코드는 어디에 둬야 하지?"가 매일의 고민이 되거든요. 그 답의 첫 스케치를 잡아 봅시다. (큰 그림을 본격적으로 짓는 건 후속 spring-boot의 몫이고, 오늘은 개념의 첫 맛만 봐요.)

가장 기본이 되는 게 계층형 아키텍처입니다. 코드를 관심사에 따라 위에서 아래로 층층이 나눠요.

텍스트
   ┌────────────────┐
   │  Presentation  │ ── 화면·API, 사용자와 만나는 곳
   └───────┬────────┘
           │ 의존
           
   ┌────────────────┐
   │  Business      │ ── 핵심 규칙, 진짜 알맹이(도메인)
   └───────┬────────┘
           │ 의존
           
   ┌────────────────┐
   │  Data          │ ── DB·파일, 데이터를 담는 곳
   └────────────────┘

화면을 그리는 층, 비즈니스 규칙을 담는 층, 데이터를 저장하는 층. 각 층이 한 가지 관심사만 맡으니, B-1에서 배운 단일 책임이 클래스를 넘어 시스템 규모로 커진 모습이에요.

그런데 여기 함정이 있어요. 화살표를 보면 의존성이 위에서 아래로, Business가 Data를 향해 흐르죠. 이러면 핵심 알맹이인 Business가 구체적인 데이터베이스에 매달리게 됩니다. DB를 다른 걸로 바꾸면 도메인 규칙까지 흔들려요. 진짜 중요한 알맹이가, 갈아 끼울 수 있어야 할 바깥(DB)에 끌려다니는 거죠.

그래서 나온 게 의존성 규칙이에요. "의존성은 안쪽(핵심 도메인)을 향해야 한다"는 원칙입니다. 어떻게요? B-2의 의존성 역전(DIP)으로 화살표를 뒤집습니다. 도메인이 "나는 이런 저장소가 필요해"라는 약속(인터페이스)을 스스로 정의하고, 바깥의 Data 층이 그 약속을 구현하게 하는 거예요. 그럼 화살표가 Data → Business 방향으로 뒤집혀, 도메인이 더는 DB에 매달리지 않습니다. 이 "의존성을 안쪽으로 뒤집는다"가 다음 Step에서 볼 헥사고날 아키텍처의 씨앗이에요.

⚠️ 언제 깨나: 작은 앱에 계층을 과하게 나누면, 기능 하나 고치려고 네 개의 파일을 열어 같은 데이터를 네 번 변환해야 하는 일이 벌어집니다. 계층은 규모가 정당화할 때 가치가 있어요. 화면-비즈니스-데이터가 뒤엉켜도 괜찮을 만큼 작은 도구라면, 굳이 삼층으로 가르지 않는 게 맞습니다.

💡 한 줄 정리

계층형 아키텍처는 코드를 관심사별 층으로 나눈다. 핵심은 의존성 규칙 — 의존성이 핵심 도메인을 향하도록, DIP로 화살표를 뒤집어 도메인을 바깥에서 떼어 낸다.

🙋 학생 질문 — "튜터님, 의존성을 뒤집으면 도메인이 인터페이스를 정의한다고 했는데, 인터페이스인데 왜 도메인 '안'에 있는 거예요?"

헷갈리기 쉬운 지점이에요. 핵심은 "인터페이스를 누가 소유하느냐"입니다. 보통은 데이터 저장을 담당하는 쪽이 "내가 저장 기능을 줄게"라며 인터페이스를 내놓는다고 생각하죠. 의존성 역전은 그 소유권을 뒤집어요. 도메인이 "나는 회원을 저장하고 찾을 수 있는 무언가가 필요해"라고 자기 말로 약속을 선언하고, 그 약속을 도메인 안에 둡니다.

그럼 바깥의 데이터 층은 도메인이 내건 그 약속을 '구현하러 오는' 입장이 돼요. 약속의 주인은 도메인이고, DB는 손님인 거죠. 그래서 의존성 화살표가 DB → 도메인으로 향합니다. 도메인은 자기가 정의한 약속만 알 뿐, 그게 어떤 DB로 구현되는지는 몰라요. 덕분에 DB를 통째로 바꿔도 도메인은 한 글자도 안 바뀝니다. "약속의 주인이 누구인가"가 의존성 방향을 정한다 — 이거 하나만 챙기시면 돼요.


Step 7: "아키텍처 맛보기 ② 헥사고날(포트 & 어댑터)"

Step 6의 "의존성을 안쪽으로 뒤집는다"를 끝까지 밀고 가면 도착하는 그림이 있어요. 헥사고날 아키텍처, 다른 이름으로 포트 & 어댑터(Ports & Adapters)입니다. 핵심 아이디어는 이래요. 코어(도메인)를 한가운데 두고, 바깥 세상과의 모든 소통을 '포트'라는 약속으로만 한다. 그럼 코어는 바깥이 어떤 기술인지 전혀 모른 채, 자기 규칙에만 집중할 수 있어요.

용어 둘만 짚을게요. 포트(Port)는 코어가 바깥에 요구하는 약속, 즉 인터페이스예요. 코어 안에 삽니다. 어댑터(Adapter)는 그 포트를 실제 기술로 구현한 부품이고, 바깥에 살아요. C-2에서 본 어댑터 패턴이, 여기선 아키텍처 전체의 뼈대로 커진 겁니다. 결제를 예로 코어부터 볼게요.

Java
// refactoring.hexagonal.PaymentPort — 포트(코어 안의 약속)
public interface PaymentPort {

    boolean pay(int amount);
}
Java
// refactoring.hexagonal.OrderService — 코어(도메인)
public class OrderService {

    private final PaymentPort paymentPort;

    public OrderService(PaymentPort paymentPort) {
        this.paymentPort = paymentPort;
    }

    public String placeOrder(int amount) {
        if (amount <= 0) {
            return "거부: 금액이 올바르지 않습니다";
        }
        boolean paid = paymentPort.pay(amount);
        return paid ? "주문 완료" : "주문 실패: 결제가 거절되었습니다";
    }
}

OrderService는 결제가 카드인지, 계좌이체인지, 어떤 회사 서비스인지 전혀 모릅니다. 오직 PaymentPort라는 약속에만 기대요. 그래서 결제 수단이 무엇으로 바뀌든 이 코어 코드는 손대지 않습니다. 바깥은 이 포트를 구현한 어댑터로 채워요.

텍스트
   바깥 세상              경계             코어(도메인)
   (어댑터)              (포트)

   HTTP 요청 ─ [입력 어댑터] ─  OrderService
                                      │
                                        PaymentPort (약속)
                                 [출력 어댑터] ─ 결제 게이트웨이
                                  · FakePaymentAdapter  (테스트)
                                  · 실제 결제 어댑터       (운영)

포트가 같은 모양이라, 테스트할 땐 가짜 어댑터를, 운영할 땐 진짜 어댑터를 꽂으면 됩니다. 코어는 그 차이를 몰라요. 그래서 외부 결제 서비스 없이도 주문 로직을 통째로 검증할 수 있습니다.

Java
@Test
void orderCompletesWhenAdapterApproves() {
    FakePaymentAdapter adapter = new FakePaymentAdapter(true);
    OrderService service = new OrderService(adapter);

    String result = service.placeOrder(10000);

    assertEquals("주문 완료", result);
    assertEquals(10000, adapter.paidAmount());
}

가짜 어댑터를 끼우니, 진짜 결제 한 번 없이 "주문이 완료되고, 코어가 만 원을 결제하려 했다"까지 확인됐어요. 눈치채셨겠지만, 이건 B-2의 DIP와 C-2의 어댑터 패턴을 아키텍처 규모로 키운 것에 다름 아니에요. 작은 원리를 큰 구조로 넓힌 거죠. 코어에 어떤 어댑터를 꽂아 줄지를 자동으로 관리해 주는 장치가 바로 후속 spring-boot의 의존성 주입 컨테이너인데, 그 본격적인 적용은 그 과목에서 만나요. 오늘은 "코어를 포트로 감싸 바깥에서 떼어 낸다"는 그림 하나만 가져가면 충분합니다.

⚠️ 언제 깨나: 작은 CRUD 앱(데이터를 넣고 빼기만 하는)에 헥사고날을 씌우면, 포트와 어댑터와 변환 계층이 잔뜩 늘어 오히려 코드가 불어납니다. 도메인이 얇은데 격식만 갖추는 꼴이에요. 이 구조의 진가는 도메인 규칙이 두텁고, 외부 연동(결제·메시지·외부 API)이 많아 갈아 끼울 일이 잦은 시스템에서 나옵니다. 규모와 복잡함이 정당화할 때 꺼내는 카드예요.

💡 한 줄 정리

헥사고날(포트 & 어댑터)은 코어를 포트(약속)로 감싸 바깥 기술에서 떼어 낸다. DIP와 어댑터 패턴을 아키텍처로 키운 것 — 가짜·진짜 어댑터를 갈아 끼워 코어를 외부 없이 검증한다.

🙋 학생 질문 — "튜터님, 계층형이랑 헥사고날 중에 뭘 써야 해요? 헥사고날이 더 좋은 거면 항상 그걸 쓰면 되나요?"

"더 좋은 것"이 아니라 "더 격식 있는 것"으로 이해하시면 정확해요. 헥사고날은 도메인을 바깥에서 가장 철저히 떼어 내는 구조지만, 그 대가로 포트와 어댑터라는 부품이 늘어요. 계층형은 더 단순하지만 도메인이 데이터 층에 끌려갈 위험이 있고요. 둘은 우열이 아니라 스펙트럼이에요. 사실 의존성 규칙을 적용한 계층형과 헥사고날은 거의 같은 곳을 향합니다 — 도메인을 안쪽에 두고 바깥을 떼어 낸다는 점에서요.

그래서 답은 늘 같아요. 규모와 복잡함을 보고 정한다. 작고 단순한 앱은 계층형으로도 충분하고, 굳이 헥사고날의 격식을 갖출 필요가 없어요. 도메인이 복잡하고 외부 연동이 많아질수록 헥사고날의 격리가 빛을 발하고요. 중요한 건 둘 다 같은 정신(도메인을 지킨다) 위에 서 있다는 거예요. 이름과 격식의 차이일 뿐. 그리고 이 판단, "언제 어느 격식을 갖추나"가 바로 다음 마지막 Step의 주제입니다.


Step 8: "클린 코드는 도그마가 아니다 — 언제 깰지가 미들의 실력"

자, 마지막 Step이에요. 이 과목 내내 각 원칙과 패턴 끝에 ⚠️ 언제 깨나를 붙여 온 거, 눈치채셨죠? 그게 우연이 아니었어요. 오늘 그 모든 경고를 하나로 모아, 이 과목의 진짜 결론을 세웁니다. "규칙은 아는데 언제 깨야 할지를 모르겠어요." 이 마지막 질문에 답하는 시간이에요.

가장 먼저 경계할 건 카고컬트(cargo cult)입니다. 원리를 모르고 형식만 흉내 내는 걸 뜻해요. "함수는 무조건 20줄 이하", "주석은 무조건 나쁨", "패턴을 많이 쓸수록 좋은 코드" 같은 맹목적 규칙 추종이 그거예요. 규칙을 외워 모든 코드에 똑같이 들이대면, 오히려 코드를 망칩니다. 지난 시간 마지막에 못 박았죠. 깨끗함은 수단이지 목적이 아니다. 규칙은 좋은 코드라는 목적지로 가는 길일 뿐, 규칙 자체가 목적이 되는 순간 길을 잃어요.

대표적인 논쟁 하나를 볼게요. 성능 대 가독성입니다. 우리는 함수를 잘게 쪼개면 읽기 좋다고 배웠어요. 그런데 함수 호출에도 아주 작은 비용이 들어서, 초당 수백만 번 도는 극한의 경로에선 그 비용이 쌓여 느려질 수 있어요. 실제로 현장엔 "클린 코드의 작은 함수 권장이 성능을 해친다"는 강한 비판이 있습니다. 같은 계산을 두 모습으로 짜 볼게요.

Java
// refactoring.tradeoff.ReadableSum — 가독성 우선
public long sumOfEvenSquares(int[] numbers) {
    long total = 0;
    for (int n : numbers) {
        if (isEven(n)) {
            total += square(n);
        }
    }
    return total;
}
// isEven(n), square(n) 같은 작은 도우미로 의도가 또렷하다
Java
// refactoring.tradeoff.InlinedSum — 성능 핫패스
public long sumOfEvenSquares(int[] numbers) {
    long total = 0;
    for (int n : numbers) {
        if ((n & 1) == 0) {
            total += (long) n * n;
        }
    }
    return total;
}

두 코드의 결과는 한 치도 다르지 않아요. 오른쪽은 도우미를 인라인으로 풀고 비트 연산을 써서, 극한 경로라면 조금 더 빠를 수 있습니다. 그런데 여기서 함정이 두 개예요. 하나, 대부분의 코드는 그런 극한 경로가 아니에요. 일 년에 몇 번 도는 코드를 비트 연산으로 비틀어 봤자, 읽기 어려워진 대가만 치를 뿐 아무도 그 속도 차이를 못 느낍니다. 둘, 측정도 안 하고 "이게 빠를 것 같아서" 가독성을 버리는 건 그냥 미신이에요. 진짜 핫패스인지는 재 보고 정해야 합니다. (얼마나 빠른지를 깊이 따지는 건 다른 과목의 몫이고, 우리에겐 "언제 이 트레이드오프를 꺼내나"라는 판단이 핵심이에요.)

그래서 결론은 이래요. 기본값은 가독성이다. 그러다 측정으로 진짜 병목이 확인된 그 좁은 구간에서만, 의식적으로 가독성을 양보한다. 이렇게 "언제 깨는지"를 아는 게 핵심입니다.

이 모든 걸 하나로 묶는 게 맥락이에요. 하루 쓰고 버릴 프로토타입과 10년 갈 코드, 혼자 짜는 스크립트와 50명이 고칠 시스템 — 같은 규칙도 맥락에 따라 답이 달라집니다. 프로토타입에 헥사고날을 씌우는 건 과잉이고, 결제 시스템에 테스트를 생략하는 건 자살골이에요. 좋은 개발자는 규칙을 맥락에 비춰 적용합니다.

텍스트
 주니어  미들, 진짜 분기점

 주니어   규칙을 외운다  모든 코드에 똑같이 들이댄다  "함수는 무조건 짧게!"
 미들     규칙의 이유를 안다  맥락을 읽는다  "여기선 짧게, 여기선 펼치는 게 맞다"

그러니까 이 과목에서 배운 모든 것 — 이름(A), 함수, 객체, 에러 처리, SOLID(B), 디자인 패턴(C), 리팩토링(D) — 은 법전이 아니라 연장통이에요. 망치가 좋다고 모든 걸 망치로 두드리지 않듯, 각 도구를 언제 꺼내고 언제 내려놓을지 아는 게 안목입니다. 그게 규칙을 외운 주니어와, 맥락을 읽는 미들을 가르는 진짜 분기점이고요.

⚠️ 언제 깨나: 가장 중요한 경고를 마지막에 둘게요. "도그마가 아니다"를 "아무렇게나 짜도 된다"로 오해하면 절대 안 됩니다. 규칙을 깨려면 먼저 그 규칙을 깊이 알아야 해요. 왜 생겼고 무엇을 지키려는지 아는 사람이 "이 맥락에선 안 맞는다"고 판단해 깨는 것과, 몰라서 그냥 못 지키는 것은 하늘과 땅 차이예요. 앞엣것은 미들의 판단이고, 뒤엣것은 그냥 못 짜는 겁니다. 깰 자격은, 규칙을 통달한 사람에게만 주어져요.

💡 한 줄 정리

클린 코드는 법전이 아니라 연장통이다. 카고컬트(규칙 맹신)를 경계하고, 맥락에 비춰 적용한다. 규칙을 외우는 게 주니어, 이유를 알아 언제 깰지 판단하는 게 미들이다. 단, 깨려면 먼저 통달해야 한다.

🙋 학생 질문 — "튜터님, 그럼 결국 '경우에 따라 다르다'가 답인 거잖아요. 그건 너무 막연하지 않아요?"

맞아요, "경우에 따라 다르다"는 말 자체는 아무것도 안 알려 주죠. 그런데 핵심은 그 뒤예요. '어떤 경우에 어떻게 다른지'를 판단할 근거를 갖추는 것, 그게 이 과목 전체가 한 일이에요.

생각해 보세요. 우리는 그냥 "유연하게 하세요"라고 한 게 아니라, 각 원칙마다 "이게 왜 좋은가(원리)"와 "언제 안 맞는가(경계)"를 짝으로 배웠어요. 그래서 막상 현장에서 긴 함수를 만나면, 막연히 고민하는 게 아니라 "이 함수가 한 가지 일만 하나? 변경 이유가 여럿인가? 지금 맥락이 마감인가 안정기인가?"를 따져 볼 수 있죠. 근거가 있는 판단과, 근거 없는 '그때그때 다름'은 전혀 달라요. 전자는 설명할 수 있고 남을 설득할 수 있지만, 후자는 그냥 감이에요.

그러니 "경우에 따라 다르다"를 두려워 마세요. 여러분은 이제 그 '경우'를 분별할 눈을 가졌으니까요. 처음엔 판단이 흔들리는 게 당연해요. 코드를 많이 읽고, 리뷰를 많이 주고받고, 자기 판단이 틀렸던 경험이 쌓이면서 그 눈이 점점 또렷해집니다. 그 여정의 출발선에 오늘 여러분이 선 거예요.


마무리

긴 여정의 마지막 페이지예요. 오늘은 원리를 현실로 가져왔습니다. 테스트 없는 레거시에 이음새를 내 손댈 틈을 만들고, 멈추지 않고 교체하는 Strangler Fig를 봤어요. 코드 리뷰에서 무엇을 먼저 보고 어떻게 건설적으로 말하는지, 코드를 담는 큰 그릇인 계층형과 헥사고날 아키텍처의 첫 스케치도 잡았고요. 그리고 무엇보다, 이 과목의 진짜 결론을 세웠습니다.

오늘 배운 핵심 세 가지

💡 하나 — 레거시엔 이음새부터. 테스트 없는 코드가 레거시다. 하드코딩된 의존성을 주입·파라미터로 열어 이음새를 내면, 테스트가 들어올 입구가 생긴다. 그 안전망 위에서야 큰 시스템도 Strangler Fig로 멈추지 않고 교체할 수 있다.

💡 둘 — 리뷰는 우선순위와 말투, 취향은 도구에. 정확성 → 설계 → 가독성 → 스타일 순으로 본다. 사람이 아니라 코드를 향해, 질문과 근거로 말한다. 스타일 논쟁은 린터·포매터에 맡겨, 사람은 로직과 설계에 집중한다.

💡 셋 — 규칙은 연장통이지 법전이 아니다. 아키텍처도 클린 코드도 맥락에 비춰 꺼낸다. 카고컬트를 경계하고, 언제 적용하고 언제 깰지 판단한다. 규칙을 외우는 게 주니어, 이유를 알아 깰 때를 아는 게 미들 — 단, 깨려면 먼저 통달해야 한다.

다음 무대로 — 그리고 이 과목을 마치며

이 모듈이 마지막이라, 예고할 '다음 시간'은 없어요. 대신 여러분이 오늘 채운 안목이 어디로 이어지는지를 짚고 인사할게요.

이 과목에서 우리는 원리와 패턴 그 자체를 익혔습니다. 그 원리들이 진짜 프레임워크라는 실전 무대에서 어떻게 살아 움직이는지는 후속 spring-boot에서 만나요. 거기서 보면 깜짝 놀랄 거예요. 의존성 주입 컨테이너가 바로 B-2의 DIP를 실현한 것이거든요. 서비스와 저장소로 계층을 나누는 건 B-1의 단일 책임이고, 관점 지향(AOP)이라 부르는 기능은 C-2의 프록시 패턴이에요. 모듈러 모놀리스와 Strangler Fig는 오늘 배운 그 전략의 실전 적용이고요. 오늘 원리로 잡아 둔 그림들이, 그 과목에서 거대한 실제 코드베이스 위에 그대로 펼쳐집니다.

그러니 이 과목은 끝이 아니라 출발선이에요. 좋은 이름과 작은 함수로 읽기 좋은 코드를 쓰고, 응집도·결합도와 SOLID로 구조를 설계하고, 검증된 패턴을 그 한계까지 알아 적재적소에 씁니다. 코드 스멜은 안전망 위에서 리팩토링하고, 무엇보다 규칙을 맹목적으로 따르지 않고 맥락에 맞게 언제 깰지 판단하는 눈을 길렀어요. 주니어에서 미들로 가는 그 분기점에, 여러분은 이제 서 있습니다. 긴 시간 함께해 줘서 고마웠어요. 좋은 코드를 보는 여러분의 안목이, 앞으로 짤 모든 코드에서 빛나길 바랍니다.


과제

오늘은 이 과목의 마지막 과제예요. 레거시에 손대고, 코어를 격리하고, 무엇보다 '판단'하는 연습입니다.

[기초] 이음새 내기

아래는 테스트를 짤 수 없는 레거시 한 토막입니다. 인사말을 만들어 콘솔에 직접 출력하는데, 두 군데가 이음새를 막고 있어요.

Java
public class GreetingBefore {

    public String greet(String name) {
        ConsolePrinter printer = new ConsolePrinter();   // 직접 생성
        LocalTime now = LocalTime.now();                 // 실행 시각에 묶임
        String hour = now.getHour() < 12 ? "좋은 아침" : "안녕하세요";
        String message = hour + ", " + name + "님!";
        printer.print(message);
        return message;
    }
}

이 코드의 막힌 이음새 두 개를 열어, 테스트 가능한 Greeting으로 바꾸세요. 출력 행위를 약속(인터페이스)으로 추상화해 주입받고, 시각을 파라미터로 받으면 됩니다. 그런 다음, 발송을 기록만 하는 가짜를 끼우고 고정된 시각을 넘겨, "오전 11시에 'jaehoon'으로 인사하면 '좋은 아침, jaehoon님!'이 나온다"를 단언하는 테스트를 한 개 작성하세요. 비즈니스 로직(인사말 형식)은 바꾸지 않는 게 핵심입니다.

[응용] 포트 & 어댑터로 코어 격리하기

주문을 받으면 재고를 확인하고 알림을 보내는 도메인이 있다고 합시다. 그런데 알림 보내는 부분이 외부 푸시 서비스에 직접 묶여 있어, 코어를 테스트하려면 진짜 푸시가 나갑니다.

알림을 보내는 행위를 포트(예: AlertPort)로 정의하고, 코어(OrderService 같은)가 그 포트에만 의존하도록 바꾸세요. 그런 다음 가짜 어댑터를 만들어, 외부 푸시 서비스 없이 "주문이 성공하면 알림이 한 번 발송된다"와 "재고가 없으면 알림이 발송되지 않는다"를 테스트로 검증하세요. 코어가 '알림이 어떻게 가는지'를 전혀 몰라야 한다는 점에 집중합니다.

[심화] 리뷰 코멘트 가려내기 + 도그마 판단

판단하는 눈을 기르는 과제예요. 코드를 짜기보다 분별하는 연습입니다.

(가) 아래 다섯 개의 리뷰 코멘트가 있습니다. 각각이 건설적인지, 아니면 무례하거나 도그마적인지 가려내고, 문제 있는 코멘트는 건설적으로 다시 쓰세요.

텍스트
① "이거 왜 이렇게 짰어요? 기본도 안 됐네요."
② "여기 입력이 비면 NPE가 날 것 같은데, 확인해 주실래요?"
③ "함수가 25줄이에요. 클린 코드는 20줄 이하랬으니 무조건 쪼개세요."
④ "이 변수명이 의도를 잘 안 드러내는 것 같아요. activeCount는 어떨까요? (nit)"
⑤ "이 패턴은 제가 싫어해서요. 그냥 빼세요."

(나) 한 동료가 "우리 프로젝트의 모든 도메인에 헥사고날 아키텍처를 적용하자"고 제안합니다. 그 프로젝트는 사내에서 쓰는 작은 데이터 조회 도구예요. 여러분이라면 이 제안에 어떻게 답하겠어요? "좋다/나쁘다"가 아니라, 무엇을 근거로 판단할지를 써 보세요. (이 과목에서 배운 "언제 깨나"를 떠올리며.)


생각해볼 주제

1. "테스트 없는 코드가 레거시다"라는 정의가 우리에게 시사하는 것

마이클 페더스는 레거시를 나이가 아니라 테스트의 유무로 정의했어요. 이 정의를 따르면, 오늘 아침에 짠 코드도 테스트가 없으면 레거시고, 10년 된 코드도 테스트가 단단하면 레거시가 아닙니다. 왜 그는 굳이 이렇게 도발적으로 정의했을까요? 이 정의가 '레거시는 남의 문제'라는 흔한 생각을 어떻게 바꾸나요? 그리고 만약 이 정의를 받아들인다면, 우리가 새 코드를 짜는 방식은 어떻게 달라져야 할까요?

2. "클린 코드가 성능을 해친다"는 비판을 어떻게 받아들일까

현장에는 클린 코드의 작은 함수 권장이 성능을 떨어뜨린다는 강한 비판이 있어요. 함수 호출에 비용이 들고, 잘게 쪼갤수록 그 비용이 쌓인다는 거죠. 이 비판은 옳을까요, 틀릴까요? 어떤 코드에서는 옳고 어떤 코드에서는 과장일까요? "측정 없이 빠를 것 같아서 가독성을 버리는 것"과 "측정으로 병목을 확인하고 가독성을 양보하는 것"의 차이는 무엇인가요? 둘을 가르는 기준을 여러분의 말로 세워 보세요.

3. "규칙을 깨려면 먼저 규칙을 통달해야 한다"는 말의 무게

이 과목은 "클린 코드는 도그마가 아니다", "언제 깰지 아는 게 미들"이라고 결론지었어요. 그런데 똑같이 "이건 상황이 달라서 규칙을 안 지켜도 돼요"라는 말을, 갓 입사한 주니어가 할 때와 10년 차 미들이 할 때, 그 무게가 전혀 다릅니다. 무엇이 그 차이를 만드나요? "규칙을 몰라서 못 지키는 것"과 "알면서 의식적으로 깨는 것"을 어떻게 구별할 수 있을까요? 그리고 아직 통달하지 못한 주니어는, 그 사이 기간을 어떤 태도로 보내야 할까요?

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안입니다. 정답이 하나뿐인 건 아니에요. 이음새를 어디에 내든, 포트 이름을 뭐라 짓든, 그 맥락에 맞으면 다른 모습도 좋습니다. 이 마지막 모듈의 진짜 채점 기준은 코드의 모양이 아니라 판단의 근거예요. "왜 이음새가 필요한가", "이 리뷰 코멘트가 왜 건설적인가", "이 규칙을 왜 지키거나 깨는가"를 설명할 수 있으면 그게 미들의 답입니다.


과제 예시답안

🎯 [과제 1 예시답안] 이음새 내기 — 테스트 가능한 코드로

채점 포인트

항목 보는 것 배점
객체 이음새 출력을 약속(인터페이스)으로 추상화해 주입받았는가 ★★★
값 이음새 시각을 LocalTime.now() 대신 파라미터로 받았는가 ★★★
행동 보존 인사말 형식을 바꾸지 않고 '받는 위치'만 옮겼는가 ★★☆
테스트 가짜 Printer를 끼우고 고정 시각을 넘겨 결과를 단언했는가 ★★★

풀이 예시

❌ Before — 출력을 직접 만들고 실행 시각에 묶여, 테스트가 들어갈 입구가 없다.

Java
public class GreetingBefore {

    public String greet(String name) {
        ConsolePrinter printer = new ConsolePrinter();
        LocalTime now = LocalTime.now();
        String hour = now.getHour() < 12 ? "좋은 아침" : "안녕하세요";
        String message = hour + ", " + name + "님!";
        printer.print(message);
        return message;
    }
}

✅ After — 출력을 약속으로 추상화해 주입받고(객체 이음새), 시각을 파라미터로 받는다(값 이음새).

Java
public interface Printer {

    void print(String message);
}
Java
public class Greeting {

    private final Printer printer;

    public Greeting(Printer printer) {
        this.printer = printer;
    }

    public String greet(String name, LocalTime now) {
        String hour = now.getHour() < 12 ? "좋은 아침" : "안녕하세요";
        String message = hour + ", " + name + "님!";
        printer.print(message);
        return message;
    }
}

이제 발송을 기록만 하는 가짜를 끼우고 고정된 시각을 넘기면, 진짜 출력 없이 결과를 단언할 수 있다.

Java
@Test
void greetsWithMorningThroughSeam() {
    RecordingPrinter printer = new RecordingPrinter();
    Greeting greeting = new Greeting(printer);

    String message = greeting.greet("jaehoon", LocalTime.of(11, 0));

    assertEquals("좋은 아침, jaehoon님!", message);
    assertEquals(1, printer.count());
}

💡 튜터의 한마디 — 이음새를 내는 변경은 '동작을 바꾸는' 게 아니라 '받는 위치를 옮기는' 것임을 꼭 기억하세요. 인사말 형식은 한 글자도 안 바뀌었죠. 그 최소한의 안전한 변경으로 입구를 연 뒤에 테스트를 채우는 겁니다. 그리고 모든 의존성을 다 뽑아 추상화하려 들지 마세요. 여기선 출력과 시각이 '테스트를 막던' 두 곳이라 연 거예요. 막지 않는 의존성까지 다 뽑으면 그게 과잉입니다.

🎯 [과제 2 예시답안] 포트 & 어댑터로 코어 격리하기

채점 포인트

항목 보는 것 배점
포트 정의 알림 행위를 포트(인터페이스)로 분리했는가 ★★★
코어 격리 코어가 포트에만 의존하고 '어떻게 가는지' 모르는가 ★★★
가짜 어댑터 외부 푸시 없이 검증할 가짜 어댑터를 만들었는가 ★★☆
두 흐름 검증 성공 시 1번 발송·재고 부족 시 0번을 모두 단언했는가 ★★★

풀이 예시

❌ Before — 코어가 외부 푸시 서비스를 직접 호출해, 테스트하면 진짜 알림이 나간다.

Java
// 코어 안에서 외부 푸시를 직접 부른다 — 가짜로 바꿔 끼울 틈이 없다
new ExternalPushService().push("주문 완료: " + quantity + "개");

✅ After — 알림을 포트로 정의하고, 코어가 그 약속에만 의존하게 한다.

Java
public interface AlertPort {

    void alert(String message);
}
Java
public class OrderStockService {

    private final AlertPort alertPort;
    private int stock;

    public OrderStockService(AlertPort alertPort, int initialStock) {
        this.alertPort = alertPort;
        this.stock = initialStock;
    }

    public String placeOrder(int quantity) {
        if (quantity > stock) {
            return "재고 부족";
        }
        stock = stock - quantity;
        alertPort.alert("주문 완료: " + quantity + "개");
        return "주문 완료";
    }
}

가짜 어댑터를 끼우면 외부 푸시 없이 두 흐름을 모두 검증할 수 있다.

Java
@Test
void orderSuccessSendsOneAlert() {
    FakeAlertAdapter adapter = new FakeAlertAdapter();
    OrderStockService service = new OrderStockService(adapter, 10);

    assertEquals("주문 완료", service.placeOrder(3));
    assertEquals(1, adapter.count());
}

@Test
void outOfStockSendsNoAlert() {
    FakeAlertAdapter adapter = new FakeAlertAdapter();
    OrderStockService service = new OrderStockService(adapter, 2);

    assertEquals("재고 부족", service.placeOrder(5));
    assertEquals(0, adapter.count());
}

💡 튜터의 한마디 — 핵심은 OrderStockService가 알림이 푸시인지 이메일인지 전혀 모른다는 거예요. 코어가 AlertPort라는 약속만 보기 때문에, 테스트에선 가짜를, 운영에선 진짜를 꽂으면 됩니다. 다만 이게 늘 옳은 건 아니에요. 알림 하나 보내는 작은 코드라면 이 정도 격식이 과할 수도 있어요. 외부 연동이 여럿이고 갈아 끼울 일이 잦을 때 포트 & 어댑터가 진가를 냅니다.

🎯 [과제 3 예시답안] 리뷰 코멘트 가려내기 + 도그마 판단

채점 포인트

항목 보는 것 배점
분별 (가) 다섯 코멘트를 건설적/무례·도그마로 정확히 갈랐는가 ★★★
재작성 (가) 문제 코멘트를 코드를 향한 질문·근거로 다시 썼는가 ★★★
판단 근거 (나) "좋다/나쁘다"가 아니라 규모·복잡도·맥락을 근거로 댔는가 ★★★

풀이 예시

(가) 다섯 코멘트 분별 + 재작성

코멘트 판정 이유
① "이거 왜 이렇게 짰어요? 기본도 안 됐네요." ❌ 무례 코드가 아니라 사람을 공격한다
② "입력이 비면 NPE가 날 것 같은데, 확인해 주실래요?" ✅ 건설적 근거(NPE) + 질문형, 코드를 향한다
③ "25줄이에요. 클린 코드는 20줄 이하랬으니 무조건 쪼개세요." ❌ 도그마 규칙을 근거로 든 카고컬트("무조건")
④ "이 변수명이 의도를 잘 안 드러내요. activeCount는 어떨까요? (nit)" ✅ 건설적 질문형 + 대안 + nit로 무게 표시
⑤ "이 패턴은 제가 싫어해서요. 그냥 빼세요." ❌ 취향·강요 근거가 '내 취향'뿐이다

문제 있는 셋을 건설적으로 다시 쓰면 이렇게 됩니다.

  • ① → "이 부분 의도가 궁금한데 설명해 주실래요? 저는 빈 입력일 때가 좀 걱정돼서요."
  • ③ → "이 함수가 두 가지 일을 같이 하는 것 같아요. 점수 계산과 등급 판정을 나누면 더 읽기 쉬울까요? (길이보다 책임을 봅니다.)"
  • ⑤ → "이 패턴이 여기선 좀 무거워 보여요. 함수 하나로도 될 것 같은데, 어떻게 생각하세요?"

(나) 작은 조회 도구에 헥사고날 전면 적용 제안에 대한 답

판단의 근거는 "좋은 구조냐"가 아니라 "이 맥락에 맞느냐"입니다. 헥사고날은 도메인이 두텁고 외부 연동이 많아 갈아 끼울 일이 잦은 시스템에서 빛나요. 사내에서 쓰는 작은 데이터 조회 도구는 도메인이 얇아서, 포트·어댑터·매핑 계층이 늘면 오히려 코드만 불어납니다. 그래서 이렇게 답하겠어요.

"방향은 좋은데, 이 프로젝트 규모엔 격식이 좀 과한 것 같아요. 지금은 계층형으로도 충분하고, 의존성 규칙만 지켜 도메인을 데이터 층에서 떼어 놓는 선이면 될 듯해요. 나중에 외부 연동이 늘어 도메인이 두꺼워지면, 그때 헥사고날 도입을 다시 보면 좋겠습니다."

💡 튜터의 한마디 — 건설적 코멘트와 도그마·취향 코멘트를 가르는 잣대는 두 가지예요. 근거가 있는가(②④는 NPE·가독성이라는 근거, ③⑤는 규칙 인용과 개인 취향뿐), 그리고 코드를 향하는가(①은 사람을 공격). (나)의 답도 같은 맥락이에요. "헥사고날은 무조건 좋다"가 카고컬트라면, "규모와 복잡도를 보고 정한다"가 미들의 판단입니다. 좋은 도구를 아는 것과, 그걸 언제 꺼낼지 아는 것은 다른 실력이에요.


생각해볼 주제

🤔 [생각해볼 주제 1] "테스트 없는 코드가 레거시다"라는 정의가 우리에게 시사하는 것

문제 상황 요약

마이클 페더스는 레거시를 나이가 아니라 테스트의 유무로 정의했다. 이 정의를 따르면 오늘 아침에 짠 코드도 테스트가 없으면 레거시고, 10년 된 코드도 테스트가 단단하면 레거시가 아니다. 왜 이렇게 도발적으로 정의했고, 이게 우리의 코딩 방식을 어떻게 바꾸는가.

튜터의 가이드 및 해설

핵심은 이 정의가 레거시를 '남의 문제'에서 '내 문제'로 당긴다는 데 있어요. "레거시는 옛날 사람이 짠 낡은 코드"라고 생각하면, 레거시는 늘 나와 무관한 먼 곳에 있죠. 그런데 "테스트 없는 코드가 레거시"라고 정의를 당기면, 방금 내가 짠 코드도 테스트가 없으면 그 순간 레거시가 됩니다. 레거시를 만드는 건 시간이 아니라 우리 자신이라는 불편한 진실을 마주하게 되는 거예요.

왜 하필 '테스트의 유무'일까요? 코드의 진짜 가치는 "지금 동작하는가"가 아니라 "앞으로도 안전하게 고칠 수 있는가"에 있기 때문이에요. 테스트가 있으면 고친 뒤 동작이 그대로인지 바로 확인할 수 있어 마음 놓고 손댑니다. 테스트가 없으면 그 확인이 불가능해 손이 묶이고, 코드는 점점 굳어 가요. 그래서 페더스에게 '고칠 수 있느냐'를 가르는 테스트의 유무가 레거시의 본질인 겁니다.

이 정의를 받아들이면 코딩 방식이 분명히 달라져요. 새 코드는 처음부터 테스트와 함께 짜서 레거시가 되는 걸 막고, 이미 테스트 없는 코드는 손댈 때마다 그 주변에 이음새를 내고 안전망을 조금씩 넓혀 가는 거죠. 레거시는 '언젠가 다 갈아엎을 대상'이 아니라 '손대는 곳부터 길들여 가는 대상'이 됩니다.

🎯 면접에선 이렇게 나온다

"레거시 코드를 어떻게 정의하나요?"라는 질문에 "오래된 코드요"라고 답하면 평범해요. "테스트 없는 코드라고 봅니다. 나이가 아니라 안전하게 고칠 수 있느냐가 기준이니까요. 그래서 새 코드엔 테스트를 함께 짜고, 레거시는 손대는 부분부터 이음새를 내 테스트를 채워 갑니다." 이렇게 정의와 실천을 같이 말하면, 레거시를 다뤄 본 사람이라는 인상을 줍니다.

💡 실무에선

신규 코드는 테스트를 자동 검사에 강제하고(테스트 없으면 병합 차단), 레거시는 전체를 한 번에 덮으려 하지 않아요. 이번에 고칠 그 부분에만 특성화 테스트로 안전망을 치고, 손댈 때마다 그 그물을 한 뼘씩 넓혀 가는 게 현실적인 길입니다.

🤔 [생각해볼 주제 2] "클린 코드가 성능을 해친다"는 비판을 어떻게 받아들일까

문제 상황 요약

현장에는 클린 코드의 작은 함수 권장이 성능을 떨어뜨린다는 강한 비판이 있다. 함수 호출에 비용이 들고 잘게 쪼갤수록 그게 쌓인다는 것이다. 이 비판은 옳은가 틀린가. 가독성을 양보해야 할 때와 그 양보가 미신일 때를 어떻게 가르나.

튜터의 가이드 및 해설

먼저 인정할 건 인정해요. 이 비판은 부분적으로 옳습니다. 함수 호출에는 아주 작은 비용이 들고, 초당 수백만 번 도는 극한의 경로에선 그게 쌓여 실제로 느려질 수 있어요. 게임 엔진의 렌더링 루프나 대용량 신호 처리 같은 곳에선 진지하게 따져야 할 문제고요.

그런데 함정이 둘 있어요. 첫째, 대부분의 코드는 그런 극한 경로가 아니에요. 일 년에 몇 번 도는 코드, 사용자 클릭에 반응하는 코드는 함수를 아무리 잘게 쪼개도 사람이 그 차이를 느낄 수 없습니다. 둘째, 현대의 컴파일러와 런타임은 작은 함수 호출을 상당 부분 자동으로 펼쳐(인라이닝) 비용을 없애 줘요. 그래서 "작은 함수는 느리다"가 늘 참인 것도 아닙니다.

그래서 진짜 기준은 측정이에요. "측정 없이 빠를 것 같아서 가독성을 버리는 것"과 "측정으로 병목을 확인하고 그 좁은 구간만 가독성을 양보하는 것"은 하늘과 땅 차이입니다. 앞엣것은 미신이에요. 느낄 수도 없는 속도를 위해 읽기 어려운 코드를 떠안는 거죠. 뒤엣것은 정당한 트레이드오프고요. 그래서 결론은 단순해요. 기본값은 가독성, 측정이 "여기가 병목"이라고 가리킬 때만 거기서 의식적으로 깬다.

🎯 면접에선 이렇게 나온다

"성능과 가독성 중 무엇을 우선하나요?"라는 질문에 한쪽만 고르면 함정에 빠져요. "기본값은 가독성입니다. 대부분의 코드는 성능 병목이 아니니까요. 다만 프로파일러로 측정해 진짜 병목이 확인된 좁은 구간에선, 거기서만 의식적으로 가독성을 양보합니다." '측정'과 '좁은 구간'을 언급하면, 조기 최적화를 경계할 줄 아는 사람으로 보입니다.

💡 실무에선

성능이 의심되면 추측하지 말고 프로파일러로 핫스팟부터 찾아요. 전체 코드의 극히 일부가 대부분의 시간을 쓰는 경우가 많아서, 거기만 최적화하면 나머지는 읽기 좋게 둘 수 있습니다. "먼저 올바르고 읽기 좋게, 그다음 측정해서 필요한 곳만 빠르게"가 순서예요.

🤔 [생각해볼 주제 3] "규칙을 깨려면 먼저 규칙을 통달해야 한다"는 말의 무게

문제 상황 요약

이 과목은 "클린 코드는 도그마가 아니다", "언제 깰지 아는 게 미들"이라고 결론지었다. 그런데 똑같이 "상황이 달라서 규칙을 안 지켜도 된다"는 말을, 갓 입사한 주니어가 할 때와 10년 차 미들이 할 때 무게가 전혀 다르다. 무엇이 그 차이를 만드나.

튜터의 가이드 및 해설

차이를 만드는 건 근거의 깊이예요. 미들이 "여긴 규칙을 깨는 게 맞다"고 할 때, 그는 그 규칙이 무엇을 지키려고 생겼는지 알고, 이 맥락에서 그 가치가 왜 덜 중요한지를 설명할 수 있어요. 잃는 것이 무엇인지도 알고 그걸 감수하기로 선택한 거죠. 반면 주니어가 같은 말을 할 때는, 솔직히 '하기 싫어서' 또는 '몰라서'를 '상황이 달라서'로 포장한 경우가 많아요. 본인도 그 차이를 잘 구분하지 못하고요.

그래서 둘을 가르는 질문은 이거예요. "깨는 이유를 설명할 수 있는가, 그리고 무엇을 잃는지 아는가." "이 규칙은 보통 A를 지키려는 건데, 여긴 B라는 맥락이라 A의 가치가 작고 대신 C를 얻는다"라고 말할 수 있으면 의식적인 선택입니다. "그냥 이게 더 편해서요"밖에 안 나오면, 그건 깨는 게 아니라 못 지키는 거예요.

그럼 아직 통달하지 못한 주니어는 그 사이를 어떻게 보내야 할까요? 답은 역설적이에요. 일단은 규칙을 지키되, 끊임없이 "왜"를 묻는 겁니다. 규칙을 따라 보면서 그게 무엇을 지켜 주는지 몸으로 익히고, 어겼다가 아파 본 경험을 쌓는 거죠. 그 "왜"가 충분히 쌓였을 때, 비로소 "여긴 달라"를 근거 있게 말할 자격이 생깁니다. 깰 자격은 통달한 사람에게만 주어진다는 말은, 주니어에게 "규칙을 무시하라"가 아니라 "이유를 물으며 지켜라"라는 뜻이에요.

🎯 면접에선 이렇게 나온다

"클린 코드 규칙을 항상 지키나요?"라는 질문에 "항상 지킨다"도 "상황 따라 다르다"도 약해요. "규칙이 왜 생겼는지를 먼저 이해하려 합니다. 보통은 지키되, 깰 때는 무엇을 얻고 무엇을 잃는지 설명할 수 있을 때만 깹니다. 그 근거를 코드 리뷰나 주석에 남기고요." '근거를 남긴다'까지 말하면, 규칙을 의식적으로 다루는 사람이라는 인상을 줍니다.

💡 실무에선

규칙을 어길 땐 PR 설명이나 주석에 "왜 여기선 이렇게 했는지"를 한 줄 남겨요. 그 한 줄을 적을 수 있다는 것 자체가 의식적인 선택이라는 증거고, 적을 말이 떠오르지 않으면 그건 그냥 규칙을 어긴 거예요. 미래의 동료(그리고 미래의 나)는 그 한 줄 덕분에 "이건 실수가 아니라 의도였구나"를 알게 됩니다.

전체 목록 클린코드
이 토픽을 끝까지 봤어요 🎉 다른 트랙도 둘러보기 →