0. 필자의 디자인 패턴을 배우는 목적
회사에서 여러 프로젝트를 진행하면서 내가 작성한 코드가 더이상 나이질 기미가 안보이고 점점 스파게티로 코드 복붙 작성이 되는것 같아 어떻게 하면 고객 요구사항에 유연하게 대처하면서 코드 품질을 올릴수있을까를 생각하다고 예전 부터 공부를 하고 싶어했던 디자인 패턴 책을 구매를 하고 스터디를 하게 되었다
지금껏 읽어 왔던 개발 서적들은 저자가 말하고자 하는 바 자체를 이해하지 못하여 끝까지 읽기가 어려웠다. 물론 내 경험과 역량 부족이지만 지금까지 쌓아왔던 내 개발 경험에 비추어 보았을때는 글에서 소개되는 내용이 왜 필요하고 어떤점이 좋은지가 명확하게 판단하기가 너무 힘들었다. 그러다가 좀 더 쉽게 읽을수 있는 책을 찾다가 헤드 퍼스트라 디자인 패턴 책을 찾았다.
이 책을 잠깐 봤을때는 그림도 많고 Q&A형식으로 되어있어 이해하기가 수월해 보였다. 그중에서도 꼬리에 꼬리를 물고 깊게 이해하려는 Q&A챕터는 도움이 많이 되었다. 이 책과 다양한 인터넷 참고 자료를 통해 스터디를 하였고 공부한 내용을 기반으로 정리하여 정보 공유 목적으로 블로그 작성할 예정이다.
디자인 패턴 스터디 했었던 당시 목표는 개인 개발 역량 향상 그리고 OOP에 대해서 많은 연습도 못하였지만 개발하는 과정에서 빈번하게 발생된 디자인 문제에 대해 패턴을 만들어서 생산성을 올리고 유지보수와 가독성 좋게 코드 구조화를 잘 하는것이다 그리고 실무 현장에서 지속적이고 유연한 소프트웨어를 개발 하면서 공통적인 개발 전문 용어를 활용하여 의사결정을 하여 생산성을 높히기 위해 스터디 했다 .
우선 디자인패턴에 들어가기에 앞써 OOP 의 특징중 핵심인 다형성과 캡슐화에 대해서 한번 정리를 하겠습니다.
1. 다형성 (Polymorphism)
OOP의 목적 자체가 소프트웨어 변경에 유연한 구조를 구축하여 변화에 대응하는 제품을 만들기 위함을 가지고 있습니다. 다형성의 예시로는 메소드 오버라이딩, 부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용하는 것을 말합니다. 이를 통해 자식 클래스는 부모 클래스의 메서드를 재활용하면서, 독자적인 기능을 추가할 수 있습니다.
- 오버로딩: 같은 이름의 메서드를 매개변수의 타입이나 개수를 다르게 정의하는 것을 말합니다. 예를 들어, 같은 기능을 수행하지만 서로 다른 입력을 받을 수 있는 메서드를 만들 수 있습니다.
class Animal {
public void sound() {
System.out.println("Animal makes a sound ");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks ");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog();
myAnimal.sound(); //Animal makes a sound
myDog.sound(); //Dog barks
}
}
코드를 보면 Dog 가 Animal 을 상속받아 오버 라이드된 sound 메소드를 호출했을때 호출한 클래스의 원본을 유지하려는 오버로딩특징은 매우 중요한 OOP 특징입니다.
- 오버라이딩: 상위 클래스의 메서드를 하위 클래스에서 재정의하여 사용합니다. 이는 상속의 개념과 밀접한데, 자식 클래스가 부모 클래스의 공통적인 기능을 유지하면서도 필요에 따라 더 구체적인 동작을 구현할 수 있습니다.
class Calculator {
public int add(int a, int b) {
return a + b;
}
// 세 개의 정수를 더하는 오버로딩된 메서드
public int add(int a, int b, int c) {
return a + b + c;
}
// 두 개의 실수를 더하는 오버로딩된 메서드
public double add(double a, double b) {
return a + b;
}
}
public class Overloading {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(10, 20)); // 결과: 30
System.out.println(calc.add(10, 20, 30)); // 결과: 60
System.out.println(calc.add(10.5, 20.3)); // 결과: 30.8
}
}
예전에 배운 기초적인 개념이라 코드를 보면 개념을 한번 짚고 넘어 가는 정도로 작성을 했다(필자는 자바를 배운지 오래 되어서 간단한 개념도 정리하면서 넘어 가야한다...)
2. 캡슐화 (Encapsulation)
캡슐화는 일반적으로 변수나 메소드들을 캡슐로 안보이게 하는 정보의 은닉 개념 입니다 . 다만 캡슐화라는건 한번더 생각을 했을때 정보의 은닉이 중점입니다. 즉 캡슐화 하면 setter getter 를 많이 생각히실텐데, 좀 더 정보의 은닉 관점에서 생각을해보면 인터페이스로 추상 계층에 의존하면서 구현 계층에 대한 의존도를 낮추는것도 캡슐화에 포함이 됩니다.
간단한 예제를 작성하여 설명드리겠습니다.예제는 백기선님의 youtube 영상을 나온 Spring 코드를 조금 변형해서 설명 드리겠습니다.
public class ScoreExample {
public int getScore(String repositoryName) throws IOException {
ThirdParty thirdParty = ThirdParty.connect();
ThirdPartyRepository thirdPartyRepository = thirdParty.getRepository(repositoryName);
int score = 0;
thirdParty.addScore();
return score;
}
public void main(String[] args) throws IOException {
ScoreExample scoreExample = new ScoreExample();
int score = scoreExample.getScore("ThirdPartyService");
System.out.println("point 해당 포인트 ");
}
}
코드를 보시면 ScoreExample 클래스에서 서트파티의 정보를 연결하고 가져올수있는 getScore() 라는 메소드가 있습니다. 만약 기존 ThirdParty를 사용하지 못하고 다른 서비스로 교체를 해야한다면 어떻게 해야할까 ?
ThirdParty 에서 사용하고있는 메소드등은 전부 찾아서 에러를 해결해야할것입니다. 이러한 결합도가 높은 코드는 유지보스를 힘들게하고 테스트를 어렵게 합니다.
그렇다면 좀 더 추상화를 하여 생성과 사용을 분리해보도록 하겠습니다.
public class ScoreExample {
private ThirdPartyService thirdPartyService;
interface ThirdPartyService {
ThirdParty connect();
}
class BaseThirdPartyService implements ThirdPartyService {
@Override
public ThirdParty connect() {
return new ThirdParty();
}
}
public ScoreExample(ThirdPartyService thirdPartyService) {
this.thirdPartyService = thirdPartyService;
}
public int getScore(String repositoryName) throws IOException {
ThirdParty thirdParty = thirdPartyService.connect();
ThirdPartyRepository thirdPartyRepository = thirdParty.getRepository(repositoryName);
int score = 0;
thirdPartyRepository.addScore();
return score;
}
public void main(String[] args) throws IOException {
ScoreExample scoreExample = new ScoreExample(new BaseThirdPartyService());
int score = scoreExample.getScore("ThirdPartyService");
System.out.println("point 해당 포인트 ");
}
}
ThirdPartyService 라는 인터페이스를 만들고 이에 구현층인 BaseThirdPartyService 클래스를 생성하여 ScoreExample 객체 생성시 주입을 해주었습니다.객체의 생성과 사용이 분리가 되어 ScoreExample 클래스는 ThirdPartyService 라는 인터페이스를 참조하고있어 BaseThirdPartyService 가 아닌 다른 클래스로 교체가 되어도 다른 로직들은 변경이 안되기 때문에 결합도가 낮은 코드구조를 갖게되어 유지보수 및 테스트 하기 좋은 코드가 되었습니다.
클래스이 생성과 사용만 분리는 관점에서 내부 구현 클래스 정보가 외부로 부터 영향을 미치지 않기 때문에 캡슐화가 지켜지고 있습니다.
그리고 캡슐화는 정리했을때 아래와 같은 특징이 있습니다.
- 정보 은닉 (Information Hiding): 외부에 노출되어야 할 부분과 숨겨져야 할 부분을 명확히 구분하여 유지보수성과 안전성을 높입니다. 이는 단순히 getter/setter 메서드를 제공하는 것이 아니라, 객체의 내부 구현 방식을 변경하더라도 외부에 영향을 미치지 않도록 설계하는 데 중점을 둡니다.
- 의존성 역전 원칙 (Dependency Inversion Principle): 상위 모듈(인터페이스 또는 추상 클래스)은 하위 모듈(구체 클래스)에 의존하지 않고, 반대로 하위 모듈이 상위 모듈에 의존하게 설계하는 것이 캡슐화의 중요한 부분입니다. 이로 인해 구현체를 쉽게 교체하거나 확장할 수 있는 유연한 구조를 제공합니다.
- 불변성 (Immutability): 객체의 상태를 변경할 수 없도록 만들어 예기치 못한 상태 변화를 방지하는 것도 캡슐화의 일환입니다. 불변 객체를 활용하면 멀티스레드 환경에서도 안전한 코드를 작성할 수 있습니다.
여러 디자인 패턴에서 이를 개념을 활용하여 활용하고 있습니다. OOP 의 특징중 가장 핵심이 되는 다형성과 캡슐화에 대해서 해서 기본적인 설명하여 앞으로 디자인 패턴을 배우는데 도움이 될수있수있는 밑거름이 되면 좋을것 같습니다.
'개발이야기 > Design Pattern' 카테고리의 다른 글
객체 지향 SOLID 원칙 - DIP (0) | 2024.11.01 |
---|---|
객체 지향 SOLID 원칙 - ISP (1) | 2024.10.31 |
객체 지향 SOLID 원칙 - OCP (4) | 2024.10.29 |
객체 지향 SOLID 원칙 - SRP 편 (3) | 2024.10.28 |