들어가며
특정 라이브러리, 최신 프로그래밍 언어, 인프라 기술 등은 '기술'로 분류되고,
이런 기술을 학습하는 것은 알고나면 당연한게 많기 때문에 '개발'을 공부하는 게 좋다: OOP, 테스트, 아키텍쳐 등
책 초반부터 찔리는 내용들이 상당히 많음..ㅋㅋ
<OOP>
1. 객체지향의 핵심 가치인 역할, 책임, 협력이 무엇인가?
-> 역할: 역할은 객체가 시스템 내에서 수행하는 기능이나 작업을 의미한다. ex. 은행 시스템에서 '계좌' 객체는 잔액 관리, 입금, 출금 등의 역할을 수행할 수 있다.
-> 책임: 책임은 객체가 수행해야 하는 행위와 상태를 관리하는 의무를 말한다. 또한 객체가 알아야 하는 정보와 수행할 수 있는 행동을 포함한다. ex. '계좌' 객체에서 계좌 잔액을 정확히 유지하고, 거래 내역을 기록하며, 잔액 부족 시 출금을 거부하는 등의 책임을 가질 수 있다.
-> 협력: 협력은 객체들이 상호작용하며 시스템의 기능을 수행하는 방식을 의미한다. ex. 은행 시스템에서 '고객' 객체가 '계좌' 객체에 출금 요청을 보낼 수 있다.
2. 진행하는 프로젝트를 설명해보고, 객체지향인지 아닌지 어떻게 판단할 수 있는가?
-> 객체지향적인지 판단하기 위해서는 다음과 같은 특징들을 살펴볼 수 있다: (온라인 쇼핑몰 프로젝트에서)
- 추상화: 복잡한 시스템을 간단하고 이해하기 쉬운 객체들로 모델링했는지 확인 -> ex. '상품', '고객', '주문', '장바구니' 등의 실제 개념을 추상화하여 클래스로 모델링했는지
- 캡슐화: 객체의 내부 상태와 구현 세부사항을 외부로부터 숨기고, 필요한 인터페이스만 노출하는지 확인 -> ex. 각 클래스가 자신의 데이터를 캡슐화하고 필요한 메서드만 외부에 노출하는지
- 상속: 객체 간의 계층 구조를 통해 코드 재사용과 확장성을 제공하는지 확인 -> ex. '일반상품'과 '할인상품' 과 같은 클래스들이 '상품' 클래스를 상속받아 구현되었는지
- 다형성: 동일한 인터페이스를 통해 다양한 객체들이 자신만의 방식으로 동작할 수 있는지 확인 -> 결제' 인터페이스를 통해 '신용카드결제', '계좌이체', 'PayPal결제' 등 다양한 결제 방식을 다형적으로 처리하는지
- 객체 간 통신: 객체들이 메시지를 주고받으며 상호작용하는지 확인
- 단일 책임 원칙: 각 객체가 명확하고 단일한 책임을 가지고 있는지 확인
- 느슨한 결합: 객체들 간의 의존성이 최소화되어 있는지 확인
3. 객체지향이 무엇인가? 클래스를 이용해서 프로그래밍하는 방법 외에(ㅋㅋ) 어떤 설명을 할 수 있는가?
-> 2번
4. 객체와 클래스의 차이점은 무엇인가? 붕어빵과 붕어빵 틀의 관계(ㅋㅋ;;)라고 설명하지 말고.
-> 객체는 클래스(설계도)를 바탕으로 실제로 만들어진 인스턴스이다. 그래서 클래스는 메모리에 직접 할당되지 않고 객체를 생성하기 위한 '틀'로써 객체의 속성(데이터)과 행위(메서드)를 정의하며 컴파일 시 사용된다. 객체는 클래스의 추상적 정의를 구체화하여 실제 데이터와 상태를 가지며, 런타임에 생성되고 실제로 메모리에 할당되어 프로그램에서 사용된다.
5. 메서드는 왜 메서드라고 부르는가? 함수와는 어떤 차이가 있는가?
-> 함수는 독립적으로 존재하며, 어떤 특정 객체에 속하지 않는다. 반면 메서드는 객체에 종속되어 있어 객체를 통해 호출되며, 객체의 상태에 접근할 수 있다. 또한 오버라이딩과 오버로딩을 통해 다형성을 지원할 수 있다.
<테스트>
1. 테스트를 학습한다는 것은 무슨 의미인가? Mockito, JUnit, H2 학습 외에 어떤게 있는가?
-> 책 초반에도 말했듯이, 단순히 테스트 도구나 프레임워크 외를 물어본 것 같다.
a) 성능 테스트:
- 부하 테스트: 시스템이 예상되는 최대 부하 상황에서도 정상적으로 작동하는지 확인한다. ex. 웹 서버가 동시에 N명의 사용자 요청을 처리할 수 있는지
- 스트레스 테스트: 시스템의 한계를 넘어서는 극단적인 조건에서 시스템의 동작을 확인한다. ex. 데이터베이스 연결이 끊어졌을 때 애플리케이션이 어떻게 대응하는지
- JMeter, Gatling: 대량의 가상 사용자를 생성하여 시스템에 부하를 가하고, 응답 시간, 처리량 등을 측정한다.
b) 보안 테스트:
- OWASP 가이드라인: 웹 애플리케이션의 일반적인 보안 취약점(예: SQL 인젝션, 크로스 사이트 스크립팅)을 확인하고 방어
- 침투 테스트: 실제 해커의 공격을 시뮬레이션하여 시스템의 보안 취약점을 찾아내는 테스트
- 퍼지 테스트: 보안 쪽에 포함되는지는 잘 모르겠지만, 요번에 진행하는 오픈소스 프로젝트에서 하고 있더라. 용어만 들어봤었는데, 자동화된 도구를 사용해 프로그램 또는 프로그램 기능에 유효하지 않거나 예상치 못한 입력을 제공하고 나서, 결과를 확인해서 프로그램이 충돌하거나 부적절하게 작동하는지 확인하는 테스트라고 함. 힙 버퍼 오버플로우나 use-after-free 와 같은 c/c++ 및 임베디드 코드에서 메모리 손상을 찾는 데 특히 효과적이라고 한다.
c) 테스트 자동화:
- CI/CD 파이프라인에 테스트 통합: 코드 변경이 있을 때마다 자동으로 테스트를 실행하여 즉시 피드백을 제공 -> Jenkins, GitLab CI와 같은 도구들로 코드 커밋, 빌드, 테스트, 배포 등의 과정을 자동화할 수 있다.
d) 테스트 데이터 관리:
- 테스트 데이터 생성 전략: 테스트에 필요한 다양한 시나리오의 데이터를 효과적으로 생성하고 관리하는 방법
- 데이터베이스 시딩: 테스트 실행 전에 데이터베이스에 미리 정의된 데이터를 삽입하여 일관된 테스트 환경을 제공
e) 테스트 더블 기법:
- Stub: 미리 정의된 응답을 반환하는 간단한 대체 객체
- Spy: 실제 객체의 일부 메서드만 오버라이드하여 호출을 기록하거나 수정
- Mock: 예상되는 호출과 응답을 미리 프로그래밍한 객체로, 실제 호출이 예상과 일치하는지 검증
- Fake: 실제 구현을 단순화한 가벼운 버전의 객체(예: 인메모리 데이터베이스)
f) 계약 테스트:
- 서비스 제공자와 소비자 사이의 인터페이스 계약을 정의하고 검증한다.
- Consumer-Driven Contracts: 소비자가 기대하는 서비스의 동작을 계약으로 정의하고, 제공자가 이를 만족하는지 테스트 -> Spring Cloud Contract와 같은 마이크로서비스 간의 계약을 정의하고 검증하는데 사용되는 도구가 있다.
2. 단위 테스트, 통합 테스트, end to end 테스트의 차이점이 무엇인가? 또한 단위 테스트에서 말하는 단위는 무엇이고, 통합 테스트에서 말하는 통합은 무엇인가?
-> 단위 테스트: 개별 코드 단위(주로 메서드 수준)의 정확성을 검증한다. 외부 의존성을 모킹 또는 스터빙 해주어야 한다.
-> 통합 테스트: 여러 단위가 함께 올바르게 작동하는지 검증한다. 특히 여러 컴포넌트 또는 모듈 간의 상호작용을 검증하는 데 사용하기 때문에, 실제 의존성을 사용하며 단위 테스트보다 느리고 복잡하다.
-> End-to-End 테스트: 전체 시스템의 흐름과 기능성을 검증한다. 사용자 관점에서의 전체 애플리케이션을 테스트하며, 실제 환경과 유사한 조건에서 수행하기 때문에 가장 복잡하고 시간이 오래 걸린다.
- 단위 테스트에서의 '단위': 주로 메서드나 함수 수준을 의미하는 것 같고, 이 단위가 독립적으로 테스트 가능해야 하며, 외부 의존성은 모킹하거나 스터빙해야 한다.
- 통합 테스트에서의 '통합': 여러 컴포넌트나 모듈이 함께 작동하는 것을 의미하고, 실제 의존성을 사용하여 여러 부분이 올바르게 결합되어 작동하는지 확인해야 한다. ex. 데이터베이스와의 상호작용, 외부 서비스와의 통신, 여러 레이어 간의 상호작용
3. Mockito, JUnit, H2 없이 테스트를 작성해야할 때, 스프링 프로젝트에 테스트를 어떻게 넣을까?
-> 모르겠다.. 직접 구현해야하지 않을까..?
<아키텍쳐>
1. 아키텍쳐라고 하면 떠오르는 것이 무엇인가? SOLID? SOLID를 알고 있다면 의존성 역전과 의존성 주입이 어떻게 다른지 설명해라.
-> 의존성 역전: 모듈 간의 결합도를 낮추고 유연성을 높이기 위해 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 설계 원칙이다.
// 의존성 역전 적용 전
class EmailService {
public void sendEmail() { /* 이메일 전송 로직 */ }
}
class NotificationManager {
private EmailService emailService = new EmailService();
public void notify() {
emailService.sendEmail();
}
}
// 의존성 역전 적용 후
interface MessageService {
void sendMessage();
}
class EmailService implements MessageService {
public void sendMessage() { /* 이메일 전송 로직 */ }
}
class NotificationManager {
private MessageService messageService;
public NotificationManager(MessageService service) {
this.messageService = service;
}
public void notify() {
messageService.sendMessage();
}
}
-> 의존성 주입: 의존성 역전 원칙을 구현하는 방법 중 하나로, 객체의 의존성을 외부에서 제공하는 테크닉이다. 생성자 주입, 세터 주입, 인터페이스 주입 등의 방식이 있다.
class NotificationManager {
private MessageService messageService;
public NotificationManager(MessageService service) {
this.messageService = service;
}
public void notify() {
messageService.sendMessage();
}
}
// 사용
MessageService emailService = new EmailService();
NotificationManager manager = new NotificationManager(emailService);
2. 의존성이 무엇인가?
-> 의존성은 한 모듈이나 클래스가 다른 모듈이나 클래스를 필요로 하는 관계를 말하며, 만약 A가 B에 의존하게 되면 B의 변경이 A에 영향을 줄 수 있다.
3. 인터페이스가 하나 있고, 인터페이스의 구현체는 프로젝트에 하나밖에 없다면, 굳이 엔지니어가 구현체와 인터페이스를 분리한 이유가 무엇일까?
-> 일단은 '무엇을 할 것인가'(인터페이스)와 '어떻게 할 것인가'(구현체)를 분리함으로써 코드의 구조를 개선할 수 있고, 이후 다른 구현체가 필요할 때 쉽게 추가할 수 있으며 리팩토링이 쉬워진다.
-> 그 외에도 인터페이스를 사용하면 목(mock) 객체를 쉽게 만들 수 있어 단위 테스트가 용이해진다는 장점이 있다고 함.
1부. 객체지향
01. 절차지향과 비교하기
- 순차지향 프로그래밍(sequential oriented programming)과 절차지향 프로그래밍(procedure=함수 oriented programming): 순차지향 프로그래밍은 어셈블리어로 작성된 코드와 같이 함수의 개념이 존재하지 않아 코드를 위->아래로 순차적으로 읽는다. 만약 목적지 주소를 건너뛰어야 하는 경우가 있으면 jmp나 goto와 같이 프로그램의 실행 위치를 옮기는 방식으로 코드가 동작한다. 반면, 절차지향 프로그래밍은 함수를 만들어 프로그램을 만드는 방식으로, 일반적인 c언어로 작성된 코드가 이에 해당된다.
- 코드 1.6: 책 예시에 있는 코드 1.4, 1.5 방식으로만 작성해왔던 것 같은데, 수익을 RestaurantChain - Store - Order - Food 으로 수익 계산 로직을 객체가 처리하도록 변경되었다. 단순히 food.getPrice()로 계산하지 않고 food 내부에 수익을 리턴하는 함수 calculateRevenue()를 만들고 price를 리턴하도록 함으로써 비즈니스 로직을 객체가 처리하도록 변경했다.
- 객체지향으로 코드를 작성하는 이유가 가독성을 높이기 위해서는 아니다. 객체지향은 가독성보단 객체 자체가 맡고 있는 책임에 더 집중한다. 그렇다고 해서 '책임'이라는 개념이 객체지향만의 특징은 아니다.
- 코드 1.10: 수익을 계산하는 Calculable 인터페이스를 추가하고, 기존 클래스들이 인터페이스를 구현하도록 변경했다. 즉, 객체 각각에 할당되어 있던 책임을 인터페이스로 분할하게 됐는데, 이 쪽이 더 자연스러워 보이고 이해하기 쉬웠다. C언어는 이러한 추상적인 인터페이스 개념을 지원하지 못하므로 절차지향 언어라고 한다.
- 객체지향에서는 엄밀히 말해 '책임을 객체를 추상화한 역할에 할당'하는 것을 중요하게 보고, 책임을 어떻게 나누고 어디에 할당하는지 집중한다.
- 코드 1.12: Food 외에 새로운 수익 아이템이 추가되는 경우, 기존 Calculable 인터페이스로 분리하지 않았다면 계산 로직 자체를 변경해야 했을 것이다. 하지만 Calculable 인터페이스를 사용하게 되면 리스트로 한번에 묶어 코드 변경 없이 기능을 확장할 수 있다.
- 코드 1.14: canAfford 메서드 파라미터로 Product를 전달하면 더 깔끔하지 않은가 잠깐 생각했는데, Account 클래스에서 금액 비교를 할 때, Product name 등은 의미가 없고 오히려 Product 객체에 의존하게 된다는 단점이 있는 것 같다.
02. 객체의 종류
- Value Object: 코드 2.2에서 Color은 객체이지만 동시에 값이다. 값은 불변성(final, 순수함수)/동등성(equals, hashCode)/자가검증(생성자) 이라는 특징을 가지고 있는데, 이 세가지 특징을 만족할 때 VO라고 부른다. = 신뢰할 수 있는 객체
- 코드 2.4: 모든 멤버 변수가 final로 선언되어 있더라도 참조타입이라면 불변성이 보장되지 않을 수 있다. 즉, 불변 객체 내부의 참조 객체가 불변이 아니라면 그 객체는 불변이 아니다.
- 코드 2.6: 또한, VO의 모든 함수는 매번 동일한 값을 반환하는 순수 함수여야 한다.
- VO를 만드는 이유에 대해 더 집중하는 게 좋다. VO의 불변성으로 객체를 신뢰할 수 있게 만들 수 있다.(멀티 스레드 환경)
- VO에서 값과 상태가 같으면 같은 객체로 봐야한다. 실제로는 객체를 비교하는 상황이 많지 않아 equals/hashCode를 오버라이딩하는 것을 생략하기도 한다. 동등성도 결국 VO의 신뢰성을 위한 것이므로, 신뢰할 수 있는 객체라면 생략할 수 있다고 본다. (+ 단 VO는 id와 같은 식별자 필드를 멤버 변수로 가지고 있어서 안된다.)
- 값객체를 만들 때, 롬복의 @Value 어노테이션을 사용할 수 있다. - equals/hashCode 자동생성, 멤버변수 및 클래스 final 선언
- java16부터 record 키워드를 사용하여 데이터를 담는 간단한 클래스를 만들 수 있다. - equals/hashCode 자동생성, 멤버변수 및 클래스 final 선언/getter 자동 생성
- Data Transfer Object: 다른 객체의 메서드를 호출하거나 시스템을 호출할 때, 매개변수를 나열하는 것이 불편하니, 데이터를 구조적으로 만들어 전달하기 위한 객체
- 데이터를 전달하는 데만 집중하며, 그 밖의 역할이나 책임을 가지지 않는다. 데이터 읽기/쓰기 외의 비즈니스 로직이 들어가면 안된다.
- 롬복의 @Data 어노테이션을 사용할 수 있다. - getter/setter/toString/EqualsAndHashCode와 같은 어노테이션이 한 번에 지정된다.
- Data Access Object: 데이터베이스 접근과 관련된 역할을 지닌 객체로, 데이터베이스와의 연결/CRUD 연산 수행/보안 취약성을 고려한 쿼리 작성 과 같은 역할을 담당한다. = 스프링에서 Repository 개념 = 도메인 로직과 데이터베이스 연결 로직을 분리하기 위함
- Entity:
- Domain Entity: Domain Model 중 특화된 비즈니스 로직이나 생애주기를 가지는 등, 특별한 기능을 가지는 모델을 말한다. 식별자와 비즈니스 로직을 갖는다.
- DB Entity: 데이터베이스에서 정보를 담는 객체를 표현하기 위한 수단을 말한다.
- JPA Entity: 관계형 데이터베이스에 있는 데이터를 객체로 매핑하는 데 사용되는 클래스를 말한다. = 영속성 객체
- 데이터베이스와의 연동을 추상화하고, 데이터 접근을 담당함으로써 애플리케이션의 비즈니스 로직과 영속성 로직을 분리하기 위함
- @Entity 어노테이션을 지정하여 사용한다.
03. 행동
어떤 클래스를 만들 때(ex. 자동차)
클래스에 필요할 것으로 예상되는 속성(=데이터 위주의 사고) vs 클래스가 어떤 행동을 해야하는지(=행동 위주의 사고)
- 인터페이스와 행동은 다르다. 인터페이스는 외부에서 어떤 객체에게 행동을 시키고자 할 때, 어떤 행동을 지시하는 방법이라고 할 수 있다. 따라서 별도의 지시자가 없으면 public으로 동작한다.
- 인터페이스로 객체 간의 통신이 이루어질 때, 코드가 실행되기 전까지는 실제로 어떤 메서드가 호출될지 알 수 없다.