프로그래밍/객체지향
[스터디2] 02. 행동 및 SOLID
mandus
2024. 12. 15. 00:27
목차
3. 행동
- 객체지향을 구분 짓는 요인은 데이터가 아닌 행동임. 행동이 객체를 결정함.
// 데이터를 보고 클래스 이름 생각하기
public class ??? {
private float speed;
private float direction;
}
// 행동을 보고 클래스 이름 생각하기
public class ??? {
public void ride() {
// ...
}
public void run() {
// ...
}
public void stop() {
// ...
}
}
3.1 덕 타이핑
- 덕 타이핑(duck typing): 덕 텍스트에서 유래함. 행동이 같다면 같은 클래스로 부르겠다는 의미임.
- 덕 텍스트: 만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
- 추가적으로, 덕 타이핑과 비슷한 개념으로 구조적 타이핑이 있음. 구조적 타이핑은 객체의 행동을 기반으로 타입을 판단하지만, 정적 타입 언어에서 컴파일 타임에 타입 검사를 수행한다는 점에서 차이가 있음.
# 덕 타이핑(Python)
# 객체가 특정 행동(메서드)을 할 수 있다면, 그 객체는 해당 타입으로 간주됨.
class Duck:
def quack(self):
print("Quack!")
class Dog:
def quack(self):
print("I'm a dog, but I can quack!")
def make_quack(animal):
animal.quack()
duck = Duck()
dog = Dog()
make_quack(duck) # 출력: Quack!
make_quack(dog) # 출력: I'm a dog, but I can quack!
// 구조적 타이핑(TypeScript)
// 객체의 행동뿐 아니라, 정해진 구조를 만족해야 타입으로 간주됨.
interface Quackable {
quack(): void;
}
class Duck implements Quackable {
quack() {
console.log("Quack!");
}
}
class Dog {
quack() {
console.log("I'm a dog, but I can quack!");
}
}
function makeQuack(animal: Quackable) {
animal.quack();
}
const duck = new Duck();
const dog = new Dog();
makeQuack(duck); // 출력: Quack!
makeQuack(dog); // 출력: I'm a dog, but I can quack!
특징 | 덕 타이핑 | 구조적 타이핑 |
타입 검사 시점 | 런타임 | 컴파일 타임 |
언어 | 동적 타입 언어 (Python, Ruby, JavaScript) | 정적 타입 언어 (TypeScript, Go, Scala) |
타입 선언 필요 여부 | 불필요 (행동만 맞으면 됨) | 불필요 (구조와 행동이 맞으면 됨) |
유연성 | 높음 (런타임에 행동만으로 판단 가능) | 제한적 (구조와 행동 모두 컴파일 타임에 검증됨) |
안정성 | 낮음 (런타임 에러 발생 가능) | 높음 (컴파일 타임에 에러 검출 가능) |
3.2 행동과 구현
- 행동을 고민하면서 구현이나 알고리즘을 고민하지 말고, 순수하게 이 클래스에 어떤 동작을 시킬 수 있을 것인지 고민하는 것이 좋음.
- 자바의 인터페이스를 활용하면 구현 없이도 메서드를 정의할 수 있음.
- 시스템 개발 초기에 팀원들과 함께 역할과 행동을 기반으로 인터페이스를 정의함. → 각 객체들이 어떻게 협력할지를 합의함. → 각자 본인이 맡은 부분을 구현함. 이때, 인터페이스는 반드시 지켜야 함. 이것을 확인하기 위해 사용하는 것이 테스트임.
// 행동을 표현하는 인터페이스
public interface Car {
void drive();
void changeDirection(float amount);
void accelerate(float speed);
void decelerate(float speed);
}
3.3 인터페이스
- 인터페이스와 행동은 다름. 인터페이스는 어떤 행동을 지시하는 방법의 집합임.
- 대표적인 인터페이스
- API(Application Programming Interface): 애플리케이션을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것
- UI(User Interface): 사용자가 프로그램을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것
// 자바 8 이전 버전에서 인터페이스에 private 선언이 있으면 컴파일 에러가 발생함.
// 자바 9버전부터는 인터페이스에 private 메서드를 사용할 수 있음.
package com.example.demo.book.car;
public interface Car {
private void igniteEngine();
}
// 인터페이스의 메서드에 public을 지정하면 IntelliJ IDEA 같은 IDE(Integrated Development Environment: 통합 개발 환경)에서는 public으로 선언할 필요가 없다고 함.
// 인터페이스에서 선언되는 메서드는 public이 기본이기 때문임.
package com.example.demo.book.car;
public interface Car {
public void igniteEngine();
}
3.4 행동과 역할
- 예)
- C: 자동차 클래스를 만들어 줄 수 있나요?
- S: 자동차는 어떤 행동을 하는 객체인가요?
- S: 꼭 자동차이어야 하나요?
- S: 자동차라는 클래스를 만들어서 달성하려는 목표가 뭐지요?
- C: 탑승할 수 있고, 달릴 수 있으면 좋겠네요. → 탈것
- C: 자동차 클래스를 만들어 줄 수 있나요?
- 자동차 클래스를 만들고 자전거와 말 클래스를 만들어 달라는 요청을 받았을 때, 구현에 집중한 코드는 확장되는 요구사항에 유연하게 대처할 수 없음.
- 책에서는 추상화나 다형성을 일부러 피해서 설명함.
3.5 메서드
- 함수의 각 입력값은 정확히 하나의 출력값으로 대응됨. 같은 입력에 대해 같은 출력을 해야 함. 따라서 객체지향에서는 협력 객체에 어떤 일을 요청할 때 함수를 실행한다라는 말 대신 메시지를 전달한다라고 표현함.
- 객체는 협력 객체에 메시지만 보낼 뿐이고 실제로 어떤 방법(method)으로 일을 처리할지는 객체가 정함. 메서드란 어떤 메시지를 처리해 달라는 요청을 받았을 때 이를 어떻게 처리하는지 방법을 서술하는 것임.
4. SOLID
- SOLID: 로버트 C. 마틴이 2000년대 초반에 고안한 5가지 원칙
- 단일 책임 원칙(SRP: Single Responsibility Principle)
- 개방 폐쇄 원칙(OCP: Open-Closed Principle)
- 리스코프 치환 원칙(LSP: Liskov Substitution Principle)
- 인터페이스 분리 원칙(ISP: Interface Segregation Principle)
- 의존성 역전 원칙(DIP: Dependency Inversion Principle)
- 각 원칙의 목표는 소프트웨어의 유지보수성과 확장성을 높이는 것임.
- 설계 관점에서 유지보수성을 판단할 때 사용하는 3가지 맥락
- 영향 범위: 코드 변경으로 인한 영향 범위가 어떻게 되는가? → 축소
- 의존성: 소프트웨어의 의존성 관리가 제대로 이뤄지고 있는가? → ○
- 확장성: 쉽게 확장 가능한가? → ○
4.1 SOLID 소개
4.1.1 단일 책임 원칙
- 클래스에 너무 많은 책임이 할당되어서는 안 되며, 단 하나의 책임만 있어야 함.
- 변경으로 인한 영향 범위를 최소화하는 것이 목적임.
- 예) 8,000라인으로 구성된 클래스와 같이 긴 코드는 책임이 제대로 분할되지 않은 경우가 많음.
- 이때, 책임이라는 개념은 관점에 따라 다르게 해석될 수가 있음.
- 책임을 프론트엔드 개발자, 백엔드 개발자로 분류
- 책임을 프론트엔드 개발자, 백엔드 개발자, 시스템 운영자로 분류
- 책임을 시스템 개발자로 분류
- 단일 책임 원칙에서 말하는 책임은 액터(actor)에 대한 책임임. 시스템에서 어떤 모듈이나 클래스를 사용하게 될 액터가 여럿이라면 단일 책임 원칙에 위배됨.
- 단일 책임 원칙의 목표
- 클래스가 변경됐을 때 영향을 받는 액터가 하나여야 함.
- 클래스를 변경할 이유는 유일한 액터의 요구사항이 변경될 때로 제한되어야 함.
4.1.2 개방 폐쇄 원칙
- 클래스의 동작을 수정하지 않고 확장할 수 있어야 함. 즉, 확장에는 열려 있고 변경에는 닫혀 있음.
- 따라서 코드를 확장하고자 할 때 취할 수 있는 최고의 전략은 기존 코드를 아예 건드리지 않는 것임.
- 확장하기 쉬우면서 변경으로 인한 영향 범위를 최소화하는 것이 목표임.
- 코드를 추상화된 역할에 의존하게 만듦으로써 이를 달성할 수 있음.
4.1.3 리스코프 치환 원칙
- 바바라 리스코프에 의해 고안됨.
- 기본 클래스의 계약을 파생 클래스가 제대로 치환할 수 있는지 확인함.
- 예) Rectangle 클래스를 상속하는 Square 클래스
- 초기 코드 작성자가 생각하는 모든 의도를 테스트 코드로 만들면, 파생 클래스를 작성하는 개발자는 테스트를 보고 초기 코드 작성자의 의도를 파악할 수 있으며 기본 클래스로 작성된 테스트에 파생 클래스를 추가해 테스트할 수 있음.
4.1.4 인터페이스 분리 원칙
- 클라이언트가 자신이 사용하지 않는 인터페이스에 의존하지 않아야 함. 즉, 어떤 클래스가 자신에게 필요하지 않은 인터페이스의 메서드를 구현하거나 의존하지 않아야 함.
- 개발자들이 하나의 인터페이스로 모든 것을 해결하려고 할 때 위배됨. 인터페이스가 통합되면 인터페이스의 역할이 두루뭉실해짐.
- 예) BeanAware와 BeanNameAware의 역할
- 또한, 통합된 인터페이스는 구현체에 불필요한 구현을 강요할 수도 있음.
- 기능적 응집도를 추구하는 것임.
- 예) JPA에서 CORS 설계에 따라 Repository를 Reader와 Writer로 인터페이스를 세분화할 수도 있음. 더 나아가 CRUD를 기준으로 세분화할 수도 있음. 원칙은 원칙일 뿐이지, 분리를 하지 않아도 Repository로 개발을 잘 하고 있기 때문에 고민해야 함.
4.1.5 의존성 역전 원칙
- 고수준/저수준 모듈이 추상화에 의존해야 함.
- 고수준 모듈은 추상화에 의존해야 함.
- 고수준 모듈이 저수준 모듈에 의존해서는 안 됨.
- 저수준 모듈은 추상화를 구현해야 함.
4.2 의존성
- 의존: 다른 객체나 함수를 사용하는 상태
- 사용하기만 해도 의존하는 것임.
- 마찬가지로 어떤 객체나 코드를 사용하기만 해도 결합이 생기는 것임.
4.2.1 의존성 주입
- 의존성 주입은 @Autowired 애너테이션을 사용하는 것이다.(△) → 의존성 주입은 프레임워크 없이 사용할 수 있음.
- 스프링의 도움 없이는 의존성 주입을 사용할 수 없다.(X)
- 의존성 주입(Dependency Injection): 필요한 의존성을 외부에서 넣어주는(주입) 것
- 예) 파라미터(매개변수) 주입, 생성자 주입, 수정자 주입
- 의존성 주입은 의존성을 제거하지 않고 약화시킬 뿐임.
- 자바에서 new를 사용하면 더 이상 다른 객체가 사용될 여지가 사라지므로 사실상 하드 코딩이고, 강한 의존성을 만듦. new를 사용하는 것은 Content coupling에 해당하는 결합이며, 정보 은닉을 위반함.
- 전달하는 의존성이 꼭 객체일 필요도 없고, 람다식을 건네줄 수도 있음.
4.2.2 의존성 역전
- 대부분의 소프트웨어 문제는 의존성 역전으로 해결이 가능하다는 말이 있을 정도로 5가지 원칙 중에서 가장 중요한 원칙임.
- 의존성 역전(Dependency Inversion): 화살표의 방향을 바꾸는 기법
- 세부사항에 의존하지 않고 정책에 의존하도록 코드를 작성함.
- 경계를 만드는 기법이며, 모듈의 범위를 정하고 상하 관계를 표현하는 데 사용할 수 있는 수단임.
- 시스템을 설계할 떄 상위 모듈은 하위 모듈에 의존해서는 안 됨. 의존성 역전은 상위 모듈이 하위 모듈에 의존하지 않게 하고 싶을 때 사용할 수 있는 기법임.
- 구현보다 추상에 의존하는 것이 좋음.
4.2.3 의존성 역전과 스프링
- Q. 스프링은 의존성 주입을 지원하는 프레임워크인가요?
- Q. 스프링은 의존성 역전 원칙을 지원하는 프레임워크인가요?
- A. 스프링은 의존성 주입을 지원하는 프레임워크이지만, 의존성 역전 원칙을 지원하는 프레임워크는 아님. 의존성 역전 원칙은 설계의 영역임.
4.2.4 의존성이 강조되는 이유
- 3가지 맥락에 문제가 있을 때 해결 방법
- 영향 범위에 문제가 있다면 응집도를 높이고 적절히 모듈화해서 단일 책임 원칙을 준수하는 코드를 만듦.
- 의존성에 문제가 있다면 의존성 주입과 의존성 역전 원칙 등을 적용해 약한 의존 관계를 만듦.
- 확장성에 문제가 있다면 의존성 역전 원칙을 이용해 개방 폐쇄 원칙을 준수하는 코드로 만듦.
- 소프트웨어 설계를 잘하고 싶다면 코드를 변경하거나 확장할 때 영향받는 범위를 최소화할 수 있어야 함.
- 복잡한 의존성과 정리되지 않은 의존성은 스파게티 코드로 이어짐.
4.3 SOLID와 객체지향
- SOLID가 추구하는 방향과 객체지향이 추구하는 방향은 다름.
- 객체지향의 핵심: 역할, 책임, 협력
- SOLID: 객체지향 방법론 중 하나로, 변경에 유연하고 확장할 수 있는 코드를 만드는 데 초점을 둠. 즉, 높은 응집도와 낮은 결합도가 목표임.
4.4 디자인 패턴
- 1994년에 4인조(GoF)라 불리는 컴퓨터 공학자에 의해 고안된 디자인 패턴은 총 23가지임.
- 크게 생성(creational) 패턴, 구조(structural) 패턴, 행동(behavioral) 패턴으로 분류할 수 있음.
- 생성 패턴: 객체 생성을 유연하고 효율적으로 처리하는 방법
- 구조 패턴: 객체를 조합해서 더 큰 구조를 형성하는 방법
- 행동 패턴: 객체간의 행위와 역할을 조정하는 방법
- 디자인 패턴이 어떤 식으로 생겼고 예제를 외우는 것이 아니라, 어떤 상황에서 어던 문제를 해결하는지 이해하는 것이 효율적인 학습 방법임. 패턴은 도구일 뿐이며, 실제로 중요한 것은 패턴에 담긴 문제 인식, 해결 과정, 해결 방법임.
이번 파트를 공부하면서 추상화와 SOLID가 무엇인지 확실하게 알고 가는 것 같습니다. SOLID를 무조건 적용하는 게 좋다고 생각했는데, 이 부분에 대해 고민해 봐야겠습니다.
이 글은 『자바/스프링 개발자를 위한 실용주의 프로그래밍』 책을 학습한 내용을 정리한 것입니다.