지난 주 토비의 스프링 3.1 셋트를 구입했습니다.
1년전 스프링을 처음 배웠었는데 당시에는 따라하기에 급급해서 원리를 이해하려기보다는 공식화해서 프로젝트를 완성하는 것에 목적이었습니다. 그래서 처음으로 다시 돌아가서 기초를 다지려고 합니다.
스프링(Spring)
스프링은 프레임워크(Framework)이다.
프레임워크는 다음과 같은 뜻을 지니고 있고 그 뜻과 같이 소프트웨어 개발에 필요한 뼈대 역할이 되어준다.
- (건물 등의) 뼈대
- (판단, 결정 등을 위한) 틀
- 체제, 체계
(네이버 어학사전 참고)
개발자에게 스프링은 개발을 빠르고 효율적으로 할 수 있게 도와주는 도구이다.
하지만 쉽게 생각하고 스프링을 기계적으로 또는 어떤 공식과 같이 사용하는 것은 개발자에게 독이 될 수 있다. 스프링이 추구하는 가치와 핵심을 모른다면 말그대로 도구에서 끝나버릴 수 있기 때문이다.
우리는 스프링의 장점과 가치를 알고 효율적으로 사용하기 위해서 스프링을 제대로 공부해볼 필요가 있다.
스프링은 자바를 기반으로 한다. 따라서 객체지향 프로그래밍(OOP)이 기반이 된다.
오늘 정리할 내용은 객체지향 설계 원칙(SOLID) 중 하나인 개방 폐쇄 원칙이다.
개방 폐쇄 원칙(OCP, Open-Colosed Principle)
개방 폐쇄 원칙(이하 OCP)이 추구하는 바는 이러하다.
"자신의 확장에는 열려 있어야 하며, 주변 변화에 있어서는 닫혀있어야 한다."
나는 이 이론의 이해를 돕기 위해서 카페라는 곳을 생각해봤다.
카페라는 애플리케이션이 있다면 이 애플리케이션은 OCP가 지켜져야 한다고 생각한다. 왜냐하면 카페는 커피를 제조하고 판매도 해야되며, 홍보도 할 수 있어야 하기 때문이다.
☞ 확장에 열려있음
또한 메뉴가 바뀐다고 해도 커피는 제조될 수 있어야 하며 판매원(또는 파트타이머)이 바뀌더라도 판매할 수 있어야 한다.
☞ 변화에 닫혀있음
사실 구구절절 설명을 들어봐도 직접해보는 것이 백번 낫다고 생각한다.
여기 OCP가 지켜지지 않은 코드가 있다.
이 코드는 카페관리자가 운영중인 카페A와 카페B에서 만들 수 있는 커피를 보여준다.
카페A와 카페B는 아메리카노를 공통적으로 만들고 있다. 하지만 서로 다른 메뉴를 보유하고 있다.
CafeA.java
package spring.chap1;
public class CafeA {
public void makeCoffee(String name) {
if ("Americano".equals(name)) {
System.out.println("Americano를(을) 만듭니다.");
} else if ("Latte".equals(name)) {
System.out.println("Cafe Latte를(을) 만듭니다.");
} else {
System.out.println("메뉴를 입력해주세요.");
}
}
}
CafeB.java
package spring.chap1;
public class CafeB {
public void makeCoffee(String name) {
if ("Americano".equals(name)) {
System.out.println("Americano를(을) 만듭니다.");
} else if ("Dutch".equals(name)) {
System.out.println("Dutch Coffee를(을) 만듭니다.");
} else {
System.out.println("메뉴를 입력해주세요.");
}
}
}
CafeAdmin.java
1
2
3
4
5
6
7
8
9
10
11
12
|
package spring.chap1;
public class CafeAdmin {
public static void main(String[] args) {
CafeA cafeA = new CafeA();
CafeB cafeB = new CafeB();
cafeA.makeCoffee("Latte");
cafeB.makeCoffee("Dutch");
}
}
|
cs |
위 코드의 문제점은 무엇일까?
두 카페의 운영자 CafeAdmin 클래스에서는 Cafe(A,B) 인스턴스를 생성 후 makeCoffee 메소드를 호출해 사용하고 있다.
동작을 하는데 있어 문제는 없지만 만약 서로 다른 카페에 새로운 메뉴가 추가적으로 발생된다면 makeCoffee 메소드에 if문은 서로 다르게 또 끝도 없이 길어질 수 있다. 즉, 변화에 있어서 닫혀있지 못한 코드이다.
그렇다면 어떻게 이 코드를 OCP가 지켜질 수 있도록 수정할 수 있을까?
일단 공통적인 부분을 추려내보도록 하자.
일반적으로 카페에서는 커피를 만든다는 공통점을 가지고 있다.
그럼 이 공통된 부분을 인터페이스로 만들 수 있지 않을까?
CafeCommon이라는 인터페이스를 만들고 makeCoffee() 메소드를 선언했다.
CafeCommon.java
|
package spring.chap1;
public interface CafeCommon {
public void makeCoffee();
}
|
cs |
그리고 CafeCommon 인터페이스를 CafeA와 CafeB가 상속받도록 했다.
인터페이스를 상속받았기 때문에 CafeA와 CafeB는 makeCoffee가 반드시 구현되어야 한다.
(추상클래스로 선언하는 경우 구현을 강요받지는 않지만 여기서는 생략하기로 한다)
자, 이제 변경된 CafeA, CafeB 클래스와 CafeManage 클래스를 같이 보도록 하자.
CafeA.java
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package spring.chap1;
public class CafeA implements CafeCommon {
private String name = "";
public void setCoffee(String name){
this.name = name;
}
public void makeCoffee() {
System.out.println(name + "를(을) 만듭니다.");
}
}
|
cs |
CafeB.java
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package spring.chap1;
public class CafeB implements CafeCommon{
private String name = "";
public void setCoffee(String name){
this.name = name;
}
public void makeCoffee() {
System.out.println(name + "를(을) 만듭니다.");
}
}
|
cs |
CafeAdmin.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package spring.chap1;
public class CafeAdmin {
/**
* @param args
*/
public static void main(String[] args) {
CafeManage cm = new CafeManage();
cm.setCoffee("Americano");
cm.makeCoffee();
cm.setCoffee("Dutch Coffee");
cm.makeCoffee();
}
}
|
cs |
인터페이스를 상속받은 CafeA, CafeB 클래스에는 카페 관리자로부터 제조할 커피의 이름을 받아 private 변수에 넣어주는 setCoffee 메소드가 추가되었다. 이로써 우리는 if문을 하나씩 추가해주지 않아도 원하는 커피를 얼마든지 만들어낼 수 있게 되었다. 즉, 변경이 발생해도 기본 구조는 수정되지 않기 때문에 변화에 닫혀 있다고 볼 수 있으며 커피를 만드는 기능 이외에 다른 기능들도 쉽게 추가해서 사용할 수 있기 때문에 확장에 열려있다고 할 수 있다.
+피드백은 언제나 환영입니다 :)