형식과 자료 구조
- 2.1 형식을 맞추는 목적
- 2.2 세로 형식
- 2.3 가로 형식
- 2.4 팀 규칙
- 2.5 자료 추상화
- 2.6 자료/객체 비대칭
- 2.7 디미터 법칙
- 2.8 자료 전달 객체(DTO)
코드의 형식과 자료 구조는 둘 다 "어떻게 보여주느냐"의 문제야. 형식은 코드의 시각적 구조로 의사소통하고, 자료 구조는 데이터를 어떤 방식으로 노출하느냐로 설계의 방향이 갈려.
코드의 형식은 단순한 미적 취향이 아니라 의사소통의 도구야. 들여쓰기, 줄 바꿈, 공백, 정렬 — 사소해 보이지만 코드를 처음 보는 사람이 가장 먼저 보는 게 형식이거든. 오늘 짠 코드의 기능은 다음 릴리스에서 바뀔 수 있어. 근데 코드의 가독성은 앞으로 바뀔 코드의 품질에 지속적으로 영향을 미쳐. 원래 코드는 오래 살아남지 못하지만, 코드의 스타일과 규율은 살아남거든.
세로 형식의 핵심 비유는 "신문 기사처럼 작성하라"야. 신문 기사는 위에 표제가 있고, 첫 문단에 핵심 내용이 나오고, 아래로 갈수록 세부사항이 나와. 소스 파일도 이래야 해. 이름은 간단하면서도 설명적으로, 위쪽에는 고차원 개념과 알고리즘, 아래쪽에는 저차원 함수와 세부 내역. 패키지 선언, import, 각 함수 사이에 빈 행이 있으면 시각적으로 개념이 분리되고, 빈 행 하나가 새로운 개념이 시작됨을 알려주는 시각적 단서 역할을 해.
연관된 코드는 세로로 가까이 놓아야 해. 서로 밀접한 개념 사이에 주석이나 빈 행을 넣으면 연관성이 끊어지거든. 함수 A가 함수 B를 호출하면 두 함수는 가까이 있어야 하고, 변수는 사용하는 위치에 가깝게 선언하고, 인스턴스 변수는 클래스 맨 처음에 선언해. 종속 함수는 호출하는 함수를 먼저 배치하고 호출되는 함수를 그 아래에 배치하면 소스 코드가 자연스럽게 고차원에서 저차원으로 읽혀. 이게 "내려가기 규칙"의 실천이야. 직접 호출 관계가 없더라도, 비슷한 동작을 수행하는 함수들은 가까이 놓아.
한 행은 얼마나 길어야 할까? 저자는 120자 정도를 권장해. 오른쪽으로 스크롤해야 할 정도로 긴 행은 피해. 공백을 써서 밀접한 개념과 느슨한 개념을 구분하는 것도 중요해. 할당 연산자(=) 양쪽에는 공백을 넣어서 왼쪽과 오른쪽이 분리됨을 보여주고, 함수 이름과 괄호 사이에는 공백을 안 넣어서 함수와 인수가 밀접하다는 것을 보여줘.
가로 정렬 — 변수 선언에서 타입과 변수명, 할당값을 세로로 정렬하는 건 하지 마. 정렬하면 변수 타입은 안 보고 변수명만 읽게 되고, 할당문에서 오른쪽 값만 보게 되니까 오히려 코드의 진짜 의도를 놓치게 돼. 정렬이 필요할 정도로 선언이 많다면, 클래스를 쪼개야 하는 신호야. 들여쓰기는 소스 파일의 계층 구조를 표현하는 핵심 수단이고, 짧은 if 문이나 함수에서 들여쓰기를 무시하고 싶은 유혹이 생겨도 뿌리쳐야 해.
결국 가장 중요한 건 이거야 — 개인의 스타일보다 "팀의 규칙이 우선"이야. 팀에 속했다면 팀이 합의한 규칙을 따라야 해. 모든 팀원이 같은 형식으로 코드를 쓰면, 소프트웨어가 일관적인 스타일을 보여주거든. 스타일이 매번 다르면 읽는 사람이 "이건 누가 짰지?"를 매번 파악해야 하고, 그건 불필요한 인지 부하야.
형식이 코드의 겉모습이라면, 자료 구조는 코드의 속 구조야. 객체와 자료 구조는 정반대인데, 많은 개발자가 이 둘을 구분 없이 써. 변수를 private으로 선언하는 이유가 뭘까? 남들이 변수에 의존하지 않게 하려고지. 근데 getter/setter를 달아버리면 구현을 외부에 노출하는 거나 마찬가지야. 좌표를 getX(), getY()로 노출하는 구체적인 클래스와, getR(), getTheta()(극좌표)로 노출하는 추상적인 인터페이스를 비교해봐. 후자는 내부가 직교좌표인지 극좌표인지 알 수 없어. 그게 핵심이야 — "구현을 감추려면 추상화가 필요하다". 단순히 변수 사이에 함수라는 계층을 넣는 게 아니라, 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진짜 추상화거든. getFuelTankCapacityInGallons()보다 getPercentFuelRemaining()이 나아. 전자는 내부에 연료 탱크 변수가 있다는 걸 노출하지만, 후자는 퍼센트라는 추상적 개념만 제공하니까.
여기가 핵심인데. 객체는 추상화 뒤로 자료를 숨기고, 자료를 다루는 함수만 공개해. 자료 구조는 자료를 그대로 공개하고, 별다른 함수를 제공하지 않아. 이 둘은 근본적으로 상호 보완적이야. 절차적인 코드(자료 구조를 사용하는 코드)는 "새로운 함수를 추가하기 쉬워" — 기존 자료 구조를 변경하지 않고 함수만 추가하면 되거든. 반면 새로운 자료 구조를 추가하기는 어려워 — 모든 함수를 고쳐야 하니까. 객체 지향 코드는 "새로운 클래스를 추가하기 쉬워" — 기존 함수를 변경하지 않고 새 클래스만 추가하면 돼. 반면 새로운 함수를 추가하기는 어려워 — 모든 클래스를 고쳐야 하니까. 결론 — "모든 것이 객체"라는 건 미신이야. 때로는 단순한 자료 구조에 절차적인 코드가 가장 적합한 상황이 있어.
"모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다" — 이게 디미터 법칙의 핵심이야. final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); 이런 코드를 **기차 충돌(Train Wreck)**이라고 불러. 여러 객차가 한 줄로 이어진 기차처럼 보여서 그래. 내부 구조를 줄줄이 들여다보고 있는 거거든. 이걸 분리한다고 해서 문제가 해결되는 건 아니야. 핵심은 ctxt에게 "뭘 하라고 시키는 것"이야. outputDir이 필요한 이유가 임시 파일을 생성하기 위해서라면, ctxt.createScratchFileStream(classFileName) 이렇게 하면 ctxt가 내부 구조를 알아서 처리해. 디미터 법칙도 지키고, 내부 구조도 숨기고. 다만, ctxt가 자료 구조라면 당연히 내부 구조를 노출해도 돼. 디미터 법칙은 객체에만 적용되는 거야.
자료 구조체의 전형적인 형태가 "DTO(Data Transfer Object)"야. 공개 변수만 있고 함수가 없는 클래스. 빈(Bean) 구조는 비공개 변수에 getter/setter로 조작하는 건데, 일종의 사이비 캡슐화야. 활성 레코드는 DTO의 특수한 형태로, save()나 find() 같은 탐색 함수도 제공해. 문제는 여기에 비즈니스 로직까지 넣으려는 유혹이야. 그러면 자료 구조도 아니고 객체도 아닌 잡종이 되거든. 활성 레코드는 자료 구조로 취급하고, 비즈니스 로직은 별도 객체에 넣어.
정리
2장 읽고 기억할 거 세 가지:
- 신문 기사처럼 쓰고, 팀 규칙을 따르라. 위에서 아래로 고차원에서 저차원으로 흐르게 하고, 개인 취향보다 일관성이 가독성을 만든다
- 객체와 자료 구조는 정반대다. 새 타입을 추가할 일이 많으면 객체, 새 함수를 추가할 일이 많으면 자료 구조가 유리하다. "모든 것이 객체"는 미신이다
- 기차 충돌을 피하라. 객체의 내부 구조를 줄줄이 타고 들어가지 말고, 객체에게 뭘 하라고 시켜라. 디미터 법칙은 결합도를 낮추는 핵심 원칙이다