"개발자가 반드시 정복해야할 객체 지향과 디자인 패턴" 서적 기록
절차 지향과 객체 지향
절차 지향 프로그래밍
- 절차 지향 프로그래밍(Procedural Oriented): 데이터를 조작하는 코드를 별도로 분리해 함수나 프로시저로 만들어 프로그램을 구성하는 방법
절차 지향 프로그래밍은 데이터를 중심으로 프로시저가 구성된다.
- 프로시저에 의해 발생한 데이터를 다른 프로시저와 공유해 사용하기 때문에 다음과 같은 단점이 있다.
- 데이터 타입이나 의미가 변경될 때 의존하는 프로시저가 함께 수정되어야 한다.
- 같은 데이터를 각 프로시저들이 다른 의미로 사용하는 경우가 생긴다.
- 코드의 수정이 어려워지며 새로운 기능을 추가하는데 비용이 많이 든다.
객체 지향 프로그래밍
- 객체 지향 프로그래밍(Object Oriented): 데이터와 데이터와 관련된 프로시저를 하나의 객체로 묶고, 각 객체들을 모아 프로그램을 구성하는 방법
- 각 객체들은 연결되어 타 객체가 제공하는 기능을 사용할 수 있다.
- 객체는 기능을 제공하기 위해 프로시저를 사용하며, 프로시저는 자신이 속한 객체 내부의 데이터에만 접근이 가능하다.
- 객체 지향 프로그래밍은 데이터를 변경해도 해당 객체로만 변화가 집중되기 때문에 절차 지향에 비해 더 쉽게 변경이 가능하다. (즉, 유연하고 요구사항을 빠르게 반영할 수 있다.)
객체 (Object)
- 객체: 데이터와 데이터를 조작하는 프로시저(오퍼레이션, 메서드, 함수 등)로 구성
- 객체의 핵심은 객체가 어떤 데이터를 갖고 있는지가 아닌 해당 데이터를 이용한 의미있는 기능을 제공하는 것이다.
인터페이스와 클래스
- 오퍼레이션: 객체가 제공하는 기능
객체의 기능을 사용하는 것은 객체의 오퍼레이션을 사용한다는 의미이다.
- 오퍼레이션의 구성 요소
- 기능 식별 이름 (메소드 이름)
- 파라미터 및 파라미터 타입 (메소드 파라미터)
- 기능 실행 결과 값 (return 값)
3가지 구성요소를 통합해 ‘시그니처 (Signature)’라고 부른다.
- 인터페이스: 객체가 제공하는 오퍼레이션의 집합
- 인터페이스는 객체가 제공한 기능에 대한 명세서이며, 실제 구현을 포함하지 않는다.
- 클래스: 오퍼레이션을 구현하는데 필요한 데이터 및 오퍼레이션의 구현이 포함된다.
- 인스턴스: 실제 메모리에 생성된 객체
메세지
- 메세지: 객체가 다른 객체에게 오퍼레이션의 실행을 요청하는 것
Java에서 메소드를 호출하는 것이 메세지를 보내는 과정에 해당된다.
객체의 책임과 크기
- 객체는 자신마다 책임을 가지며, 한 객체가 갖는 책임을 정의한 것이 타입/인터페이스이다.
- 프로그램에서 필요한 기능들을 모두 정의하고, 객체에 이를 분배해서 할당해야 한다.
- 하나의 객체가 갖는 책임은 작을 수록 좋다. (SRP, 단일 책임의 원칙)
- 객체가 갖는 책임이 커질수록 절차 지향적으로 코드가 변질되며, 기능 변경의 어려움을 야기(경직성)한다.
- 객체의 책임이 작을수록 변경의 유연함이 증가한다.
- SRP를 고려해 설계하면, 기능 세부사항 변경 시 변경 부분이 한 곳으로 집중된다.
의존
- 의존(Dependency): 한 객체가 다른 객체를 생성하거나, 다른 객체의 메서드를 호출하거나, 파라미터로 다른 객체를 전달받는 것
- 다른 객체에 의존한다는 것은 의존하는 타입에 변경이 있을 때 자신도 함께 변경될 가능성이 높음을 의미한다.
public class FlowController {
public void process() {
...
// FileDataWriter writer = new FileDataWriter(); // 기존 생성 코드
FileDataWriter writer = new FileDataWriter(outFileName); // 기존 코드 변경
writer.write();
}
}
- 의존의 영향은 객체를 타고 계속해서 전파된다.
- 즉, 객체의 변경은 의존하고 있는 관계를 타고 전이된다.
의존의 양면성
- 의존으로 인한 변경의 영향도는 상호간에 발생한다.
- 객체 A가 변경되면 의존하는 객체 B에게 영향을 미친다.
- 객체 B의 요구사항이 변경되면 자신의 의존하고 있는 객체 A에 영향을 미친다.
// Class Authenticator
public boolean authenticate(String id, String pwd) {
...
if (member == null) return false;
return m.equalPassword(pwd);
}
// Class AuthHandler
public void handle(String id, String pwd) {
Authenticator auth = new Athenticator();
if(auth.authenticate(id, pwd)) {
...
} else {
...
}
}
e.g) Handler(객체B)에서 아이디/패스워드가 없을 때 로깅하도록 요구사항이 변경했을 때, 의존하는 Authenticator(객체 A)에 영향을 미치는 예시
// Class AuthHandler
public void handle(String id, String pwd) {
Authenticator auth = new Athenticator();
try {
auth.authenticate(id, pwd)
// 일치하는 경우 처리
} catch(MemberNotFoundException e) {
...
} catch(InvalidPasswordExecption e) {
...
}
}
// Class Authenticator
// Handler 요구사항 변경으로 인해 의존된 객체도 함께 변경됨
public void authenticate(String id, String pwd) {
...
if (member == null) throw new MemberNotFoundException();
if (!m.equalPassword(pwd)) throw new InvalidPasswordException;
}
캡슐화
- 캡슐화(Encapsuliation): 객체가 내부적으로 기능을 어떻게 구현하는지 감추는 것
- 내부의 기능 구현이 변경되어도 기능을 사용하는 코드는 영향을 받지 않는다. (내부 구현의 유연함)
절차 지향 방식 코드
public class Member {
private Date expiredDate;
public Date getExpiredDate() { return this.expiredDate; }
}
- 코드 A, B, C에서
expiredDate
를 가져와 만료 시간을 확인하는 로직이 있다고 가정할 때, 로직에 대한 요구사항이 변경되면 해당 값을 사용하는 모든 코드를 확인해 수정해야 한다.
// 코드 A, B, C에서 모두 사용
// 만료기간 로직에 대한 요구사항 변경 시 코드 A,B,C 모두 수정
if (expiredDate < currentMillis()) {
...
}
- 데이터 중심으로 프로그래밍하는 절차 지향 방식은 데이터 변화가 코드에 직접적인 영향을 미친다.
- 요구사항 변화로 인해 데이터의 구조나 쓰임새가 변경되면 데이터를 사용하는 코드를 연쇄적으로 수정해야하는 단점이 있다.
캡슐화 된 기능 구현
- 객체와 관련된 기능을 객체 내부로 캡술화하면 사용하는 코드 쪽에서는 내부 구현에 대한 변경이 중요한 것이 아닌 결과가 중요하다.
public class Member {
private Date expiredDate;
public boolean isExpired() { return this.expiredDate.getDate() < currentMillis() }
}
- 코드 A, B, C에서는 단순
isExpired()
만 사용할 뿐, 내부 로직이 변경되는 것에 크게 의미를 두지 않는다.
if(member.isExpired()) { ... }
캡슐화의 결과는 내부 구현 변경의 유연성 획득
- 기능 구현을 캡슐화하면 내부 구현이 변경되어도 기능을 사용하는 곳의 영향을 최소화할 수 있다.
- 캡슐화를 통해서 내부 구현 변경의 유연함을 얻을 수 있다.
캡슐화를 위한 두개의 규칙
Tell, Don’t Ask
- 데이터를 물어보지 않고 기능을 실행하는 규칙이다.
- 데이터 대신 기능 실행을 요청하면, 기능을 어떻게 구현했는지 여부가 내부로 감춰진다. (캡슐화)
- 데미테르의 법칙 (
Law of Demeter
)- 메소드에서 생성한 객체의 메소드만 호출
- 파라미터로 받은 객체의 메소드만 호출
- 필드로 참조하는 객체의 메소드만 호출
- 데미테르 법칙이 지키지 않는 전형적인 증상
- 연속된 get메소드 호출
value = someObject.getA().getB().getValue();
- 임시 변수에 할당된 객체의 get을 호출하는 코드가 많은 경우
A a = someObject.getA();
B b = a.getB();
value = b.getValue();
- 위와 같은 코드는 캡슐화를 약화시켜 코드 변경을 어렵게 만든다.
객체 지향 설계 과정
- 제공할 기능을 찾아 세분화하고, 해당 기능을 알맞은 객체에 할당한다.
- 기능 구현에 필요한 데이터를 객체에 추가한다.
- 기능은 최대한 캡슐화해서 구현한다.
- 객체 간에 어떻게 메세지를 주고받을지 결정한다.
- 과정1, 2를 지속적으로 반복한다.
- 위의 과정을 거치면서 객체가 기능을 제공할 때 사용할 인터페이스가 도출된다.
- 객체의 크기는 한 번에 완성되는 것이 아니며, 구현이 진행되면서 점진적으로 명확해진다.
반응형
'기술 서적 > 개발자가 반드시 정복해야할 객체 지향과 디자인 패턴' 카테고리의 다른 글
3장. 다형성과 추상 타입 (0) | 2024.06.17 |
---|