[Spring] 객체 지향 프로그래밍의 핵심인 다형성과 이를 위한 프레임워크, 스프링
객체 지향 프로그래밍이란
🧠Abstraction
💊Encapsulation
🪆Inheritance
🦠Polymorphism
애플리케이션을 객체들의 모임으로 파악하자
각각의 객체는 메세지를 주고 받고 데이터를 처리할 수 있다.
각각의 객체를 부품으로 보고,
🎯 부품을 쉽게 갈아 끼울 수 있게 만드는 것이 객체 지향의 핵심이자, 다형성이다.
다형성(Polymorphism)
다형성은 쉽게 갈아 끼울 수 있게 만드는 것이고,
그렇게 만드려면 규격(인터페이스)가 필요하다.
규격에 맞게 제작되면 갈아 끼워도 다른 것에 영향을 주지 않는다.
다른 것에 영향을 주지 않는다는 것은 내가 변화가 있을 때,
그에 따른 다른 것을 변경할 필요가 없는 것을 말한다.
이런 관계를 만드는 것을 확장 가능한 설계라고 하며,
* 결국 규격(인터페이스)를 안정적으로 설계하는 것이 중요하다.
규격을 인터페이스라 보고,
규격에 맞게 제작된 부품을 구현 객체로 본다.
혼자 있는 부품은 의미가 없듯이, 혼자 있는 객체만으로는 의미가 없다.
객체(클라이언트)와 객체(서버)는 협력 관계를 가진다.
이 협력관계에서, 객체(클라이언트)에 영향을 주지 않고 객체(서버)의 기능을 변경할 수 있게 만드는 것을
🦠다형성이라 한다.
이렇게 하면, 구현객체를 실행 시점에서 유연하게 변경할 수 있다.
또한, 객체(클라이언트)는 객체(서버)의 내부 구조를 몰라도 되니까 단순해진다.
즉, 객체끼리의 협력관계는 * 서로 규격(인터페이스)만을 알고 있다는 의미이고,
이를 통해 * 의존적이지 않은 코드를 짤 수 있다는 뜻이다.
🦁 ~에 의존한다 == ~를 알고있다
SRP (Single Responsibility Principle): 단일 책임 원칙
“하나의 클래스는 하나의 책임만 가져야 한다.”
변경이 있을 때, 파급효과🔥가 적으면 단일 책임 효과를 잘 따른 것.
즉, 하나의 변경이 있을 때 하나의 클래스의 하나의 지점만 고치면 될 때
⚠️ 책임의 범위를 잘 조정해야 한다.OCP (Open/Closed Principle): 개방 폐쇄 원칙
“확장에는 열려 있으나, 변경에는 닫혀있어야 한다.”
여기서 말하는 확장은 인터페이스를 구현한 새로운 구현 객체를 추가하는 것을 말하고,
변경에 닫혀있다는 말은 새로운 기능 추가에 기존 코드(객체, 클라이언트)는 변경되지 않아야 하는 것을 말한다.
⚠️ 이를 위해서는, 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.LSP (Liskov Substitution Principle): 리스코프 치환 원칙
“구현 객체는 기능적인 보장을 해주어야 한다.”
⚠️ 컴파일 단계의 성공을 말하는 것이 아니다.
ISP (Interface Segregation Principle): 인터페이스 분리 원칙
“작은 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.”
더 쉽게 갈아 끼울 수 있다.
DIP (Dependency Inversion Principle): 의존관계 역전 원칙
“추상화에 의존해야지, 구체화에 의존하면 안된다.”
인터페이스(규격)에만 의존해라. 즉, 객체(클라이언트)가 인터페이스만을 바라보라.
객체(클라이언트)는 구현 객체는 몰라야한다.
⚠️ 🦠다형성만으로는 OCP, DIP를 지킬 수 없다.
동작에 필요한 객체를 생성하고 연결하는 AppConfig
📜 오리.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class 오리 {
private Long id;
private 오리타입 type;
private String color;
public 오리(Long id, 오리타입 type) {
this.id = id;
this.type = type;
if(type == 오리타입.흰오리) {
this.color = "White";
} else if (type == 오리타입.청둥오리) {
this.color = "Dark Green";
} else {
this.color = "Yellow";
}
}
public Long getId() {
return this.id;
}
public 오리타입 getType() {
return this.type;
}
@Override
public String toString() {
return "{" +
"id=" + id +
", type=" + type.getValue() +
", color='" + color + '\'' +
'}';
}
}
📜 오리타입.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum 오리타입 {
흰오리("White Duck"),
청둥오리("Mallard"),
고무오리("Rubber Duck");
private String value;
오리타입(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
📜 아이템.java
1
2
3
4
public interface 아이템 {
void 작동();
}
📜 비행아이템.java
1
2
public interface 비행아이템 extends 아이템 {}
📜 날개비행아이템.java
1
2
3
4
5
6
7
public class 날개비행아이템 implements 비행아이템 {
@Override
public void 작동() {
System.out.println("The duck flies to the wings.");
}
}
📜 못나는비행아이템.java
1
2
3
4
5
6
7
public class 못나는비행아이템 implements 비행아이템 {
@Override
public void 작동() {
System.out.println("The duck can't fly.");
}
}
📜 헤엄아이템.java
1
public interface 헤엄아이템 extends 아이템 {}
📜 물갈퀴헤엄아이템.java
1
2
3
4
5
6
7
public class 물갈퀴헤엄아이템 implements 헤엄아이템 {
@Override
public void 작동() {
System.out.println("The duck swims on its web.");
}
}
📜 둥둥헤엄아이템.java
1
2
3
4
5
6
7
public class 둥둥헤엄아이템 implements 헤엄아이템 {
@Override
public void 작동() {
System.out.println("The duck floats around.");
}
}
📜 오리Repository.java
1
2
3
4
5
6
public interface 오리Repository {
void save(오리 duck);
오리 findById(Long id);
}
📜 Memory오리Repository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Memory오리Repository implements 오리Repository {
private static Map<Long, 오리> store = new HashMap<>();
@Override
public void save(오리 a오리) {
store.put(a오리.getId(), a오리);
}
@Override
public 오리 findById(Long id) {
return store.get(id);
}
}
📜 오리Service.java
1
2
3
4
5
6
7
8
public interface 오리Service {
void 오리_저장(오리 a오리);
오리 오리_가져오기(Long id);
void 오리_작동();
}
📜 오리ServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class 오리ServiceImpl implements 오리Service {
// 인터페이스에 의존하지만, 구현체에도 의존한다, 또한 다른 구현체로 갈아 끼우려면 이 코드를 변경해야 한다
// private final 오리Repository a오리Repository = new Memory오리Repository();
private final 오리Repository a오리Repository; // 인터페이스(추상화)에만 의존한다
private final 비행아이템 a비행아이템;
private final 헤엄아이템 a헤엄아이템;
// 생성자를 이용한 (구현체)주입
public 오리ServiceImpl(오리Repository a오리Repository, 비행아이템 a비행아이템, 헤엄아이템 a헤엄아이템) {
this.a오리Repository = a오리Repository;
this.a비행아이템 = a비행아이템;
this.a헤엄아이템 = a헤엄아이템;
}
@Override
public void 오리_저장(오리 a오리) {
a오리Repository.save(a오리);
}
@Override
public 오리 오리_가져오기(Long id) {
return a오리Repository.findById(id);
}
@Override
public void 오리_작동() {
a비행아이템.작동();
a헤엄아이템.작동();
}
}
📜 오리공장.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class 오리공장 {
public 오리Service a오리Service(오리타입 type) {
return new 오리ServiceImpl(a오리Repository(), a비행아이템(type), a헤엄아이템(type));
}
public 비행아이템 a비행아이템(오리타입 type) {
if (type == 오리타입.고무오리) {
return new 못나는비행아이템();
}
return new 날개비행아이템();
}
public 헤엄아이템 a헤엄아이템(오리타입 type) {
if (type == 오리타입.고무오리) {
return new 둥둥헤엄아이템();
}
return new 물갈퀴헤엄아이템();
}
private 오리Repository a오리Repository() {
return new Memory오리Repository();
}
}
📜 오리App.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class 오리App {
public static void main(String[] args){
오리공장 a오리공장 = new 오리공장();
오리Service a오리Service;
오리 a오리;
// 흰오리
a오리 = new 오리(1L, 오리타입.흰오리);
a오리Service = a오리공장.a오리Service(a오리.getType()); // 클라이언트 코드를 변경하지 않음
a오리Service.오리_저장(a오리);
오리 a흰오리 = a오리Service.오리_가져오기(1L);
System.out.println("White Duck = " + a흰오리);
a오리Service.오리_작동(); // 주입된 객체에 따라 작동이 다르다
// 청둥오리
a오리 = new 오리(2L, 오리타입.청둥오리);
a오리Service = a오리공장.a오리Service(a오리.getType()); // 클라이언트 코드를 변경하지 않음
a오리Service.오리_저장(a오리);
System.out.println();
오리 a청둥오리 = a오리Service.오리_가져오기(2L);
System.out.println("Mallard = " + a청둥오리);
a오리Service.오리_작동(); // 주입된 객체에 따라 작동이 다르다
System.out.println();
// 고무오리 생성
a오리 = new 오리(3L, 오리타입.고무오리);
a오리Service = a오리공장.a오리Service(a오리.getType()); // 클라이언트 코드를 변경하지 않음
a오리Service.오리_저장(a오리);
오리 a고무오리 = a오리Service.오리_가져오기(2L);
System.out.println("Rubber Duck = " + a고무오리);
a오리Service.오리_작동(); // 주입된 객체에 따라 작동이 다르다
}
// 출력
// White Duck = {id=1, type=White Duck, color='White'}
// The duck flies to the wings.
// The duck swims on its web.
//
// Mallard = {id=2, type=Mallard, color='Dark Green'}
// The duck flies to the wings.
// The duck swims on its web.
//
// Rubber Duck = {id=3, type=Rubber Duck, color='Yellow'}
// The duck can't fly.
// The duck floats around.
}
‘오리공장(AppConfig)’는 실제 동작에 필요한 구현 객체를 생성하고 연결하는 책임을 가지는 설정 클래스다.
이렇게 관심사를 명확히 분리하고,
생성자를 통해 * 외부에서 객체를 주입해주는 책임만을 가진 설정 클래스가 있기 때문에,
오리ServiceImpl은 추상화에만 의존할 수 있고, 이는 DIP를 지킬 수 있게 만든다.
또한, 오리의 비행 또는 헤엄 아이템을 확장하기 위해서, 오리공장(AppConfig)만 바꿔주면 되고,
오리ServiceImpl(객체, 클라이언트)은 변경하지 않아도 된다. 이는 OCP를 지킬 수 있게 만든다.
이러한 방식으로, 다형성을 극대화하고 OCP, DIP를 지켜지도록 만들어 주는 프레임워크가
스프링(Spring)이다.
스프링은 * DI와 * IoC 컨테이너를 제공함으로써 의존관계를 외부에서 주입해
다형성 + OCP + DIP를 지키는 코드를 짜도록 유도한다.
스프링이란 객체(클라이언트)의 코드 변경없이 구현 객체를 갈아 끼우는 방식으로
기능을 확장시켜 개발하게 도와주는 프레임워크이다.
org.springframework:spring-core
외부에서 객체를 주입해주는 책임을 가진 설정 클래스, Spring Container
설정정보를 바탕으로 객체를 생성하고, ⚠️ Spring Container가 관리하는 객체를 Bean이라 한다.
생성된 객체를 내부에 담아 라이프사이클 관리 및 의존성 주입을 담당하는 컴포넌트를 말한다.
🎯 객체를 낭비하지 않는다.
스프링 컨테이너🥥
는 크게 두가지 유형으로 나뉜다.
- BeanFactory
- Bean객체를 생성하고 제공하는 역할을 한다.
- 생성된 Bean 객체는 Application 내에서 *공유되어 재사용된다 (Singleton Scope).
- 또한, 실제로 *필요한 시점에서 초기화되고 (Lazy Initialization).
- 객체 간 *의존성을 자동으로 처리한다 (Dependency Injection).
- BeanFactory를 통해 객체를 생성하고 연결하는 *책임(관심사)을 분리하여 *모듈화할 수 있다 (Aspect-Oriented Programming).
📕 Bean 객체를 Singleton으로 만드는 이유
매번 클라이언트에서 요청이 올 때마다 서버에서 해당 로직을 맡은 객체를 새로 만든다고 가정했을 때,
누적되면 자원소모가 크다. (설사 Garbage Collector가 있다하더라도)
⚠️ Singleton 개념은 객체 생성 측면에서 자원소모를 효율적으로 하기 위한 디자인패턴이다. - 생성된 Bean 객체는 Application 내에서 *공유되어 재사용된다 (Singleton Scope).
- ApplicationContext ⚠️ 보통 Spring Container를 부를 때, ApplcationContext를 말한다.
- ApplicationContext는 BeanFactory를 구현하고 있어 BeanFactory의 확장된 버전이다.
- ☄️ 확장된 기능
🗡️ Environment
: 소스 설정 및 프로퍼티 값을 가져올 수 있다🗡️ MessageSource
: 메세지 설정파일을 모아, 로컬라이징을 통한 맞춤 메세지 제공 - ☄️ 확장된 기능
- –
AnnotationConfigApplicationContext
- 특정 클래스 안에 @Bean으로 선언된 메서드를 호출해서 반환된 객체를 Spring Container에 빈으로 등록
📜 AppConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class AppConfig {
@Bean
public 오리Service a오리Service(오리타입 type) {
return new 오리ServiceImpl(a오리Repository(), a비행아이템(type), a헤엄아이템(type));
}
@Bean
public 비행아이템 a비행아이템(오리타입 type) {
if(type == 오리타입.고무오리) {
return new 못나는비행아이템();
}
return new 날개비행아이템();
}
@Bean
public 헤엄아이템 a헤엄아이템(오리타입 type) {
if(type == 오리타입.고무오리) {
return new 둥둥헤엄아이템();
}
return new 물갈퀴헤엄아이템();
}
@Bean
public 오리Repository a오리Repository() {
return new Memory오리Repository();
}
}
⚠️ 사실, Spring Container는 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다.
(위의 코드는 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.)
📜 오리App.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class 오리App {
public static void main(String[] args) {
// Spring Container를 생성할 때는 구성 정보(AppConfig)를 지정해 줘야한다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// Spring Container에서 스프링 빈을 찾아 사용한다.
// 빈을 가져올 때, getBean(빈이름, 타입)으로 가져올 수 도 있고,
// getBean(이름) or getBean(타입)으로만 가져올 수 있다.
오리Service a오리Service = ac.getBean("a오리Service", 오리Service.class);
오리 a오리 = new 오리(1L, 오리타입.흰오리);
a오리Service.오리_저장(a오리);
오리 a흰오리 = a오리Service.오리_가져오기(1L);
a오리Service.오리_작동();
}
}
스프링 빈 설정 메타정보, BeanDefinition
스프링 빈 설정 메타정보가 BeanDefinition
이라는 인터페이스로써 추상화되어 있기 때문에, 스프링이 다양한 설정 형식을 지원할 수 있다.
자바코드☕
나 xml🪛
을 읽어 BeanDefinition
을 구현할 수 있다.
스프링 컨테이너 입장에서는 자바코드인지 xml인지 몰라도 되고 오직 BeanDefinition
만 알면 된다. 즉, 스프링 컨테이너는 BeanDefinition
에만 의존한다. 추상화에만 의존 설계
@Bean☕
or <bean>🪛
당 하나씩 메타정보가 생성되는데,
스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈(인스턴스)을 생성한다.
🍪 스프링 빈 등록 방법
- 직접 스프링 빈을 스프링 컨테이너에 등록하는 방법
- 팩토리 메서드를 사용하는 방법, 외부에서 특정 메서드를 호출해서 생성되는 방식
⚠️ Spring Container
는 @Bean
이 붙은 method
명을 스프링 빈의 이름으로 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ApplicationContextTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for(String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); // 빈에 대한 메타데이터 정보
// 내가 애플리케이션 개발을 위해 등록한 빈
if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
// BeanDefinition.ROLE_INFRASTRUCTURE: 스프링 컨테이너 내부에서 사용하는 빈
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name= " + beanDefinitionName + " object= " + bean)
}
}
}
}
🍪 getBean(타입)
으로만 조회 시, 같은 타입이 둘 이상 있으면 중복오류가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ApplicationContextTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로만 조회시, 같은 타입이 둘 이상 이면 중복오류가 발생한다")
void findBeanByTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("특정 타입을 모두 출력하기")
void findAllBeansByType() {
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for(String key : beansOfType.keySet()) {
System.out.println("key= " + key + " value= " + beansOfType.get(key));
}
assertThat(beansOfType.size()).isEqualTo(2);
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
}
상속관계에 있는 스프링 빈 조회
모든 자바 객체의 최고 부모인 Object 타입으로 조회하면 모든 스프링 빈을 조회한다.
🍪 부모타입으로만 조회시 자식이 둘 이상 있으면 중복 오류가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class ApplicationContextTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("TestConfig.class");
@Test
@DisplayName("부모타입으로만 조회시, 자식이 둘 이상 있으면 중복오류가 발생한다")
void findBeanByParentTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모타입으로 모두 출력")
void findAllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for(String key : beansOfType.keySet()) {
System.out.println("key= " + key + " value= " + beansOfType.get(key));
}
assertThat(beansOfType.size()).isEqualTo(2);
}
@Test
@DisplayName("부모타입으로 모두 출력 - Object")
void findAllBeansByObjectType() {
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for(String key : beansOfType.keySet()) {
System.out.println("key= " + key + " value= " + beansOfType.get(key));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
Spring의 설계 철학
🍍 POJO (Plain-Old Java Object)
특별한 제약이나 규칙을 따르지 않는 평범한 자바객체를 가리킨다.
Spring Framework는 Spring에 특화된 클래스를 요구하지 않으며,
의존성 주입을 통해 POJO 객체를 연결할 수 있는 강력한 매커니즘을 제공한다.