# 09. level interview
# 목표
우아한테크코스에서 진행한 미션의 리뷰와 피드백에 대해 정리한다.
# 레벨 인터뷰란?
- 이전 레벨에 학습한 내용에 대한 자신의 현재 상태를 파악한다.
- 자신이 알고 있는 지식을 말로 표현하는 연습을 통해
메타인지
를 기른다.
# 레벨 1에서 집중한 것
가장 중점적으로 고민한 것은 객체에게 적절한 책임을 부여하는 것이다. 그렇기 때문에 요구사항을 분석하고 고민하는데 많은 시간을 투자했고 그 결과 적절한 책임을 기반으로 빠르게 구현에 들어갈 수 있었다.
하지만 조금이라도 요구사항이 어려워지면 집중을 쉽게 포기하는 경향이 있었다. 아직 작은 단위의 문제로 접근하는 것이 어색한 탓인지 레벨 후반부로 갈 수록 쉽지 않게 다가왔다. 처음 접하는 문제는 정답이 존재하지 않는다. 수 많은 고민을 통해 보다 나은 방법을 찾아가는 것이다. 레벨 2에서는 쉽게 포기하지 않고 몰입하며 작은 문제로 나누는 연습을 진행해야 한다.
# 레벨 1 인터뷰 내용 회고하기
# TDD
실패하는 테스트를 만들고, 그 테스트가 통과하는 기능을 만들면 결국 원하는 기능이 동작하게 되는 개발 방법론이다.
# 주의할 점
TDD 시작 전에 요구사항에 대한 정리가 필요하다. 정리한 요구사항을 기반으로 객체를 떠올리며 테스트 코드 작성을 시작한다. 만약 뚜렷한 케이스가 생각나지 않는다면 더욱 작은 문제로 세분화하여 접근한다.
# 장점
- 개발자가 의도한대로 로직이 동작 하는지 명확하게 확인할 수 있다.
- 사전에 다양한 케이스를 고려하기 때문에 잠재적 오류들을 방어할 수 있다.
- 테스트하기 용이한 구조를 고민하기 때문에 객체의 구조가 깔끔해진다.
# 단점
- 개발 시간이 늘어난다.
- 이전 까지 진행한 방식과는 다르기 때문에 의식하여 사용하는데 오랜 시간이 걸린다.
# 원시값과 문자열 포장
int
값 하나 자체는 아무 의미 없는 스칼라 값이다. 만약 어떤 메서드가 int
값을 매개 변수로 활용한다면 적절한 값인지 검증하기 위한 코드들이 추가된다. 원시값을 포장
하여 객체로 활용하면 유효한 값인지 검증하기 위한 책임을 적절히 이동할 수 있다.
객체를 포장할 때 중요한 것은 필요에 의해 진행하는 것이다. 무조건 포장하기 보다 왜 포장해야 하는지 고민
해보고 하는 것이 중요하다. 정리하면 필요에 의해 객체를 포장
해야 한다는 것이다.
인터뷰 중 일부
인터뷰어
: 만약 나이가 음수인 경우 어떻게 할 것인가?
매트
: 만약 해당 변수의 책임이 커지면 포장을 고려할 것이다. 핵심은 이러한 요구사항 이전 부터 포장을 무조건적으로 고려하기 보다 변수를 가지고 있는 객체의 책임이 비대해지면 적절히 포장하여 책임을 분담하는 쪽으로 작성한다.
인터뷰어
: VO를 왜 사용해야 하는가?
매트
: 유연한 사용을 위해서 사용해야 한다.
VO를 왜 사용해야 하는가?
VO를 왜 사용해야 하는지에 대한 고민이 부족했다. 단순히 유연한 사용을 위해서 라는 답변은 추상적이기 때문에 좀 더 구체적인 결론이나 사례가 필요하다.
VO를 활용하여 도메인 객체를 생성하면 해당 객체의 책임을 적절히 분리할 수 있다. 또한 정적 팩토리 메서드를 통해 미리 인스턴스를 생성하여 캐싱으로 활용할 수 있다. VO도 객체이기 때문에 좀 더 객체지향에 가까운 프로그래밍을 진행할 수 있다.
# 한 줄에 점을 하나만 찍는다
어느 코드 한 곳에 점이 둘 이상 있다면 해당 부분은 여러 객체를 동시에 조작하는 것과 같다. 이것은 디미터 법칙(Demeter Principle)
을 어긴 것이다. 자신이 소유한 객체
, 생성한 객체
, 전달 받은 매개변수
에만 메시지를 보내야 한다. 그렇지 않은 경우 다른 객체에 깊숙이 관여하는 것과 같다. 즉 캡슐화를 어기게 된다.
인터뷰 중 일부
인터뷰어
: 디미터의 법칙은 무엇인가?
매트
: 디미터 법칙은 옆 사람에게만 물어보는 것이다.
인터뷰어
: 디미터 법칙을 어겼을 때 문제점은 무엇인가?
매트
: getter를 활용하여 단순히 조회를 진행한 뒤 메서드 내부에서 처리할 경우 해당 메서드를 가진 객체는 두 개의 객체에 의존하는 것과 같다. 객체는 의존성을 많이 가질 수록 좋지 않은 구조를 야기하기 때문이다.
디미터 법칙
디미터 법칙은 다른 객체가 어떠한 정보를 가지고 있는지 외부에서 몰라야 한다는 것을 의미한다. 즉 객체는 내부를 숨기고 해당 정보를 조작할 수 있는 행위를 활용하여 요청해야 한다.
이러한 디미터 법칙을 준수하면 캡슐화을 높이고 객체의 자율성과 응집도를 높일 수 있다.
# 일급 컬렉션
일급 컬렉션(First Class Collection)
이란 Collection을 Wrapping하며 그외 다른 인스턴스 변수가 없는 상태를 말한다. 이러한 Wrapping이 가져오는 이점은 아래와 같다.
- 도메인에 종속적인 자료구조를 만들 수 있다.
- 상태와 행위를 한 곳에서 관리할 수 있다.
- 컬렉션에 이름을 부여할 수 있다.
# VO (Value Object)
도메인에서 한 개 혹은 그 이상의 속성들을 묶어 특정 값
을 나타내기 위한 객체를 의미한다. VO를 만족하기 위해서는 몇 가지 조건이 필요하다.
equals & hash code
메서드를 재정의해야 한다.- 내부 속성을 수정할 수 없는 불변 객체여야 한다.
# VO 사용으로 얻는 이점
VO를 활용하여 객체를 생성할 경우 도메인에 종속적인 검증
을 진행할 수 있다. 또한 VO 범위가 제한된 경우 미리 인스턴스를 생성하여 캐싱
을 활용할 수 있다.
# 인터페이스 vs 추상 클래스
인터페이스
는 구현체가 모두 구현해야 한다. 100% 추상화 이기 때문에 미완성 설계도에 가깝다.추상 클래스
는 인스턴스 메서드 선언이 가능하다. 즉 일부 추상화될 수 있다. 이것은 부분 설계도에 가깝다.
인터뷰 중 일부
인터뷰어
: 인터페이스와 추상클래스의 차이에 대해서 설명해주실 수 있는가?
매트
: 언어적인 부분에서 차이는 필드를 가질 수 있는지 유무와 다중상속의 차이가 있다.
인터뷰어
: 어떨때 인터페이스를 사용하고 어떨 때 추상클래스를 사용하는가?
매트
: 우선적으로 고려하기 보다 클래스를 먼저 구현한 뒤 중복된 부분이 보이면 상속이 적절한 하다고 판단하면 적용한다.
# 상속 보단 조합을 고려하라
상속 구조는 코드를 재사용 하기 용이하지만 상위 클래스와 높은 결합도로 인하여 상위 클래스의 변경을 어렵게 만든다. 또한 상속은 클래스의 행동을 단순히 확장(extend)
하는 것이 아닌 정제(refine, 새로운 행동을 덧 붙여 기존의 행동을 부분적으로 보완하는 것)
할 때 더욱 적합하다. 이러한 상속을 오용한 예시로는 Stack
이다. Stack
은 Vector
를 상속하였기 때문에 LIFO
구조와 다르게 동작할 수 있다. 이러한 사례들로 상속을 적용할 때는 많은 고민을 동반해야 한다.
인터뷰 중 일부
인터뷰어
: 상속보다는 조합을 고려하라고 했는데 이유가 궁금하다.
매트
: 상속은 부모타입 캡슐화를 보장하지 않는다. 부모 객체가 변경되면 자식 객체가 모두 변경에 대응해야 하는 문제가 있다. 상속이 적절한 경우가 아니면 조합을 사용해야 한다.
인터뷰어
: 조합이 가지는 이점은?
매트
: 상속의 오용 중 대표적인 예시는 Stack
이다. Stack
은 기본적으로 LIFO 구조 이지만 Vector를 상속하고 있기 때문에 중간에 엘리먼트를 추가하거나 삭제할 수 있다. 즉 상속으로 인해 의도하지 않은 행위들까지 공개하게 된다.
하지만 조합을 사용할 경우 필요한 행위들만 위임을 통해 진행하기 때문에 좀 더 객체의 의도에 맞게 사용이 가능하다.
상속과 조합의 기준
그렇다면 상속과 조합의 기준은 무엇으로 나뉘게 될까? 단순히 상속을 잘못 사용했을 때의 문제만 확인했고 정작 상속과 조합을 선택하는 기준에 대해서는 크게 고민해본 적이 없었다.
상속이 적절히 사용되려면 몇 가지 조건을 만족해야 한다.
- 확장을 고려하고 설계한 확실한 is - a 관계여야 한다.
- API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파돼도 괜찮은 경우여야 한다.
즉 상속을 단순히 코드 재사용의 목적으로 사용하지 말아야 한다.
# Checked Exception, Unchecked Exception
# Checked Exception
- 반드시 처리해야 하는 예외이다.
컴파일 단계
에서 학인이 가능하다.RuntimeException 및 RuntimeException의 하위 클래스들을 제외한 나머지 예외
가 이에 해당한다.- 예시로는
IOException
등이 존재한다.
# Unchecked Exception
- 예외 처리 하지 않아도 된다.
런타임 단계
에서 확인이 가능하다.RuntimeException
의 하위 클래스들이 이에 해당한다.- 대표적으로
IllegalArgumentException
,NullPointerException
등이 이에 해당한다.
# 전략 패턴
전략 패턴은 객체들이 할 수 있는 행위를 각각의 전략 클래스로 생성한 뒤 유사한 행위들을 캡슐화
하는 인터페이스를 정의하여 행위를 유연하게 확장
하는 디자인 패턴이다.
# 생성자 주입을 통한 전략 주입
객체를 생성하는 시점에 전략이 정해진다. 만약 객체 내부에서 전략을 지속적으로 사용할 경우 추가적인 인스턴스 변수가 필요해진다. 또한 별도의 setter나 수정 관련 메서드가 없다면 생성된 객체의 전략은 변경되지 않는다.
# 메서드 실행 시점에 전략 주입
메서드 실행 시점에 전략이 주입된 경우 기존에 존재하는 인스턴스 변수를 유지할 필요가 없고 이동할 때마다 전략을 변경할 수 있다. 하지만 매번 전략을 주입 받아야 한다.
# 여러 전략을 가진 경우
객체가 가진 전략이 여러가지 인 경우 List
를 활용하여 전략을 선택하도록 할 수 있다.
# 단위 테스트하기 용이해진다
단위 테스트란? 애플리케이션에서 테스트 가능한 가장 작은 소프트웨어를 실행하여 예상대로 동작하는 것을 확인하기 위한 테스트이다.
전략 패턴을 통해 테스트하기 어려운 부분을 분리하여 단위 테스트가 가능하도록 리팩토링할 수 있다.
리뷰 중 일부
인터뷰어
: 전략 패턴을 도입할 경우 클래스가 늘어나게 된다. 사용했을 때와 안했을 때의 차이점을 고려한 것인가?
매트
: 그렇다.
인터뷰어
: 자동차 경주를 예시로 들었다. 자동차 이동 여부를 반환하는 행위를 전략으로 활용했다고 했는데 단순히 primitive tpye
을 사용해도 되지 않는가?
매트
: 어떤 타입이 매개변수에 들어오는 것을 고려하기 보다 해당 전략을 분리하며 얻을 수 있는 이점에 집중했다.
전략으로 분리하며 얻을 수 있는 이점
전략을 분리함으로 얻을 수 있는 이점에 대해 다시 한번 생각해보았다. 우선 외부에서 행위를 주입하기 때문에 확장에 매우 유연한 구조이다. 덕분에 테스트하기 용이한 구조가 된다. 또한 해당 행위의 반환 값이 복잡한 Collection과 같은 경우 전략 객체 내부에서 충분한 검증을 거치기 때문에 좀 더 신뢰할 수 있는 데이터를 받을 수 있다.
# 상태 패턴 (State Pattern)
상태 패턴은 특정 기능을 수행한 뒤 다음 상태를 반환
하는 것이다. 특정한 상태 조차 객체로 활용
할 수 있다. 동일한 메서드가 상태에 따라 다르게 동작할 수 있도록 별도의 하위 타입으로 구현한다. 같은 기능을 단순히 조건문을 활용하여 구현할 경우 상태가 추가될 때 마다 조건 분기가 함께 추가될 것이다. 하지만 상태 패턴을 사용하게 될 경우 코드의 복잡도가 증가하지 않고 상태를 추가할 수 있다.
# 의존 객체 주입 (Dependency Injection)
DI
는 필요한 객체를 직접 생성하는 것이 아니라 외부에서 주입하는 방식
이다. 객체가 스스로 의존하는 객체를 생성하는 것이아니라 외부에서 의존하는 객체를 넣어주기 때문에 의존 주입 방식
이라고 부른다.
# 의존 객체 전달 방법
생성자를 활용한 방식
: 생성자를 통해 전달 받은 객체는 인스턴스 필드에 보관한 뒤 메서드에서 사용한다.메서드를 활용한 방식
: 메서드를 통해 전달받는 의존 객체를 필드에 보관하여 사용한다.
인터뷰 중 일부
인터뷰어
: 의존 객체 주입을 사용했는가?
매트
: 사용했다.
인터뷰어
: 그렇다면 왜 직접 생성하지 않고 주입을 활용했는가?
매트
: 객체 내부를 변경하는 것 보다 클라이언트가 변경하는게 좀 더 변경하기 편한 구조라고 생각했다. 또한 강한 의존성을 같기 때문에 런타임 시점에 변경이 불가능해진다. 그렇기 때문에 상위에서 주입하는게 맞다고 생각했다.
# MVC
MVC는 애플리케이션을 세 가지 역할로 구분한 개발 방법론이다.
# Model(Domain)
Controller
가 호출할 때 요청에 맞는 역할을 수행한다. 애플리케이션의비즈니스 로직
을 구현하는 핵심 영역이다.Model
은 다른 컴포넌트(View
,Controller
)에 대해 알지 못한다. 오로지 자기 자신이수행해야 하는 행위
에 대해 서만 알고 있다.
# View
Controller
로 부터 받은Model
의 결과값을 활용하여 사용자에게 출력할 화면을 만든다.무엇을 보여주기 위한 역할
이다.View
는 다른 컴포넌트(Model
,Controller
)들에 대해 알지 못한다. 자신이 수행해야 하는지만 알고 있다.
# Controller
- 클라이언트의 요청을 받았을 때, 그 요청에 대한 실제 업무를 수행하는
Model
을 호출한다.Model
이 데이터를 어떻게 처리할지 알려주는 역할이다. - 클라이언트가 보낸 데이터가 있다면
Model
에 전달하기 쉽게 데이터를 가공한다.Model
이 해당 업무를 마치면 결과를View
에게 전달한다.
# Controller의 View 의존
View
가 Console
에 국한된 것이 아닌 웹
과 같이 다른 종류가 올 수 있는 경우가 생길 수 있다. View가 Controller를 의존하는 경우
웹으로 전환할 때 재사용 할 수 없는 구조
를 동반한다. Controller
는 직접적으로 View
에 의존하기 보다 값을 통하여 View와 통신
하는 것이 좋다.
# 도메인 객체는 소중하다
도메인은 요구사항을 해결하기 위해 매우 중요한 비즈니스 로직이 담겨 있다. 외부의 변경으로 도메인 로직이 변경되면 해당 도메인을 사용하는 애플리케이션은 어떤식으로 영향이 갈지 예측할 수 없다. 그렇기 때문에 도메인이 외부에 의존하는 것은 지양해야 한다. 이것은 직접적인 사용
과 더불어 간접적인 사용
을 통해 의존하는 것을 모두 포함한다.
# 테스트 더블
테스트를 진행하기 어려운 경우 이것을 대신하여 테스트를 진행할 수 있도록 만들어주는 객체를 의미한다.
# Fake Object
- 복잡한 로직이나 객체 내부에 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체이다.
- 동작의 구현을 가지고 있지만 실제 프로덕션에는 적합하지 않다.
# 그 밖의 피드백
질문에 대한 핵심을 잘 파악한다. 그리고 예시 기반으로 잘 답변했다. 또한 답변에 설득력이 있다. 하지만 모르는 것에 대해 직면했을 때 긴장하는 경우가 있다. 또한 인터뷰어를 쳐다보지 않고 시선을 돌리는 경우가 있다.
정리하면 평소에 고민하지 못한 포인트들을 물어보면 머릿속이 하얘진다. 무언가 틀린 답변을 하게 될 것 같다는 두려움을 가지고 있기 때문이다.
인터뷰를 하며 가장 크게 느낀 것은 틀려도 좋으니 자신의 결론과 그에 맞는 근거를 적절히 설명해야 한다는 것이다. 인터뷰는 정답을 물어보는 것이 아닌 경험과 느낀 것을 물어보는 것이다. 다음 인터뷰에서는 나만의 정의와 근거들로 알차게 채워가야 겠다.
# References.
VO(Value Ojbect)란 무엇일까? (opens new window)
[OOP] 디미터의 법칙(Law of Demeter) (opens new window)
상속보다는 조합(Composition)을 사용하자. (opens new window)