일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 알고리즘
- 동적 프로그래밍
- 스프링 핵심 원리 - 기본편
- 그래프 이론
- Til
- 네트워크 계층
- 99클럽
- 개발자취업
- Spring
- 우선순위큐
- 백준
- 완전탐색
- 그래프
- DP
- lower bound
- 데이터베이스
- 자바
- 스프링
- BinarySearch
- Java
- BFS
- 브루트포스
- 백트래킹
- 그리디
- 항해99
- 프로그래머스
- 정렬
- 트리
- DFS
- 코딩테스트준비
- Today
- Total
AtraFelis's Develop Diary
[Spring] 객체 지향 설계와 스프링 (스프링 핵심 원리 - 기본편 Section2) 본문
스프링 핵심 원리 - 기본편 강의를 수강하며 작성한 글입니다.
Section 2
스프링이란?
스프링에는 수많은 편의 기능이 존재한다.
DB 접근이 매우 편리하고, 웹 서버도 자동으로 띄워주고 수많은 기능들이 존재한다. 하지만 이것들은 스프링에 존재하는 기능이지 스프링의 진짜 핵심은 아니다.
자바의 가장 큰 특징이 무엇일까?
이 질문에 대한 답변은 대부분 동일하다. 바로 객체 지향 언어라는 것이다.
스프링 프레임워크는 자바의 이러한 특징을 바탕으로 설계되었으며, 그 핵심 목표 역시 객체 지향을 실현하는 데 있다.
즉, 스프링의 핵심은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크다.
객체지향 프로그래밍
컴퓨터 프로그램을 여러개의 독립된 단위, 즉 객체들의 모임으로 파악하고자 하는 것이다. 각각의 개체들은 고유의 역할을 부여받고, 다른 객체끼리 메시지를 주고 받으며 데이터를 처리할 수 있다.
프로그램을 작은 단위로 쪼개서 A는 이 역할만 하고, B는 이 역할만 하고 이런 식으로 역할을 구분한다고 생각하면 된다.
이러한 객체지향 프로그래밍을 하는 이유는 간단하다. 하나의 객체가 하나의 역할만을 담당하기에 프로그램을 변경하기가 쉬워진다. 특히, 대규모 소프트웨어의 개발 같은 경우에는 하나의 프로그램이 수 많은 기능을 가지므로 이런 객체지향 방식이 중요해질 수밖에 없다.
이러한 객체 지향을 실현하기 위한 중요한 특징으로는
- 추상화
- 캡슐화
- 상속
- 다형성
이렇게 네 개가 존재한다. 자바를 공부해보았다면, 이 용어들을 들어본 적이 있을 것이다. 하지만 뭔가 뜬구름 잡는 소리 같다는 생각이 드는 것이 사실이다.
상속은 실제로 처음 자바를 공부할 때 사용해볼 기회가 많고, 캡슐화는 붕어빵 틀에 비유하여 자주 설명되므로 익숙할 것이다. 하지만 추상화와 다형성은 자주 들어보기는 했는데, 그래서 이게 프로그래밍이랑 무슨 상관인데? 하기 딱 좋은 용어다. (나는 그랬다.)
역시 쉽게 와닿지 않는 무언가를 이해할 때 가장 좋은 건 간단한 예시를 하나 들어보는 것이다. 아래의 예시를 보자.
다형성은 역할과 구현을 구별하는 것이다. 자동차라는 역할이 있고 이러한 자동차의 구현체는 K3, 아반떼, 테슬라 모델3가 된다. 자바로 예시를 들어보자면, List
라는 인터페이스의 구현체로 LinkedList
, ArrayList
가 있는 것과 같다.
추상화는 사용자에게 불필요한 세부사항을 숨기고 중요한 특징만을 알려주는 것이다. 운전자는 엑셀을 밟으면 자동차가 앞으로 나간다는 사실만 알고 있으면 되지, 엑셀을 밟았을 때 자동차 내부적으로 어떤 작업을 진행하는지는 알 필요 없다. 자바에서 ArrayList
를 사용할 때 ArrayList
가 내부적으로 어떻게 값을 저장하고 있는지 몰라도 개발자는 이것을 사용할 수 있는 것과 같다.
그렇다면 이제 자바로 돌아가보자.
자바는 기본적으로 추상화와 다형성을 지원한다. 추상화는 인터페이스와 추상클래스, 다형성은 오버라이딩을 생각하면 된다.
MemberRepository 인터페이스를 구현(implements)하여 MemoryMemberRepository 와 JdbcMemberRepository 를 만들고, save()
라는 기능(메소드)를 오버라이딩한다.
이런 식으로 클라이언트인 MemberService 는 MemberRepository 라는 인터페이스만 알면 된다. 이 인터페이스가 MemoryMemberRepository 로 구현되었든, JdbcMemberRepository 로 구현되었든 MemberService 는 신경 쓸 필요가 없는 것이다.
MemberService는 그저 save()
라는 메소드만 사용할 뿐이다.
좋은 객체 지향 설계의 5가지 원칙 - SOLID
SRP (Single Responsibility Principle) : 단일 책임 원칙
하나의 클래스는 하나의 책임만을 가져야 한다는 규칙이다. 책임이라는 것 자체가 모호한 느낌이 드는데, 이것의 기준을 변경했을 때 그 파급력이 적게 만드는 것으로 하면 된다고 한다.
어떠한 클래스를 수정했을 때, 이 클래스의 영향을 받는 10개의 클래스에도 수정을 해야 한다면 매우 불편하지 않겠는가? 애초에 이러한 불편함을 없애기 위한 것이 객체 지향이니 말이다.
OCP (Open/Closed Principle) : 개방-폐쇄 원칙
소프트웨어는 확장에는 열려 있으나, 변경에는 닫혀야 한다는 원칙이다. 이건 SRP 보다도 더 모호하게 느껴진다. 해석해보자면, _코드의 변경 없이 기능을 확장하여야 한다_라는 건데, 이게 무슨 소리인가 싶다.
여기서 확장이란, 새로운 기능의 추가를 의미한다.
언제나 그렇듯 이럴 때는 예시를 보는 것이 빠르다.
public class MemberService {
private MemberRepository memberRepository = new MemoryMemberRepository();
}
아래의 상황을 가정하자. 기존에는 개발을 하는 동안 편의성을 위해 MemoryMemberRepository 를 활용했다. 그리고 이제는 개발이 얼추 끝났고 이제는 JDBC 가반의 데이터베이스를 사용해야할 떄가 왔다.
OCP가 제대로 지켜지지 않았다면, 클라이언트 코드인 MemberService 에 대대적인 수정 작업이 일어났을 것이다. ( MemberService 코드 내부에 Repository 를 설정하는 코드가 몇 백줄 있었다고 상상해보자.)
하지만 우리는 다형성을 잘 지켜가면서 개발을 해왔기에 클라이언트 코드인 MemberSerivce 자체에는 크게 수정할 것이 없다. 그저 memeberRepository 인터페이스를 implements(구현)하여 JdbcMemberRepository라는 구현체를 따로 만들면 된다.
public class MemberService {
// private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
}
이렇게 MemberRepository 의 구현체를 JdbcMemberRepository 로 변경하는 것만으로 기능을 변경하는 데 성공했다.
하지만 아쉽게도 이것조차도 완벽하게 OCP를 준수했다고 말할 수는 없다. 실제로 클라이언트 코드에 수정이 발생하기는 했으니까.
다형성 하나만으로는 OCP를 완벽하게 지킬 수 없었다.
이 원칙을 지키기 위해서는 별도의 설정자라는 것이 따로 필요하게 된다. 이것을 구현하고 실제로 적용해보는 것이 이번 강의의 핵심 목표 중 하나이다. (Section4 ~ Section5 까지 관련 내용을 쭉 다룬다.)
LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 즉, 다형성에서 하위 클래스는 인터페이스의 규약을 지켜야 한다는 말이다.
예를 하나 들어보자.
public interface player {
public void attack();
...
}
이런 player
라는 인터페이스가 있고
public class warrior implements player {
@Override
public void attack() { ... }
}
이런 구현체가 존재한다.
public static void main(String[] args) {
Player warrior = new Warrior();
warrior.attack();
}
당연히 attack()
을 했다면 공격이 나갈 것을 기대할 것이다. 상식적으로 이 warrior
라는 객체는 검을 휘둘러 상대의 체력을 깎아야 했다. 그런데 이 warrior
가 무기를 휘두르니, 상대의 체력이 회복되어 버린다. 어? 이게 대체 무슨.
당황하여 코드를 뜯어봤더니,
public class warrior implements player {
@Override
public void attack() {
무기를 휘둘러 공격력 만큼 상대의 체력을 회복시킨다.
}
}
이렇게 작성되어 있는 것이 아니겠는가.
실제로 컴파일도 되었고, 잘 작동도 한다. 하지만 이런 일이 있으면 당황스럽지 않겠는가? _attack_이라면 공격을 해야지, 왜 힐을 한단 말인가.
이런 상황을 두고 LSP를 위반했다고 한다.
ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
자동차 인터페이스 하나에 운전 기능, 정비 기능 등 전부 들어가 있는 것보다는 운전 기능만을 관리하는 운전 인터페이스, 정비 기능만을 관리하는 정비 인터페이스로 구분되는 것이 좋다는 의미다.
10,000줄 짜리 코드 하나 보다는, (잘 정리된) 100줄 짜리 코드 100개가 나은 법이다.
DIP (Dependency Inversion Principle) : 의존관계 역전 원칙
OCP와 더불어 객체 지향에서 중요한 원칙이다. 의존 관계 역전 원칙. 즉, 구현(구현 클래스)에 의존하는 것이 아니라 역할(인터페이스)에 의존해야 한다는 의미다.
저 위에 있는 자동차를 다시 한 번 생각해보자.
A라는 운전자가 있는데, 이 운전자는 자동차가 K3 아니면 운전하지 못한다. 테슬라 모델 3나 아반떼를 타면 시동 거는 법도 모르겠고 엑셀도 못 밟는다. 뭔가 이상하지 않은가?
A라는 운전자가 K3라는 구현체에 의존하는 것이다.
A라는 운전자가 자동차라는 역할 자체에 의존했다면 자동차가 K3든, 아반떼든, 테슬라 모델3든 간에 운전을 못할 이유는 없다.
프로그램도 마찬가지다.
public class MemberService {
// private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
}
이렇게 MemberService
의 _Repository_를 JdbcMemberRepository
로 변경했다. 여기서 DIP를 전혀 지키지 않았다면, 코드 여기저기서 빨간불이 들어오면서 오류가 마구잡이로 나타날 것이다.
물론, 이 코드는 OCP와 마찬가지로 DIP를 완벽하게 지켰다고 할 수는 없다. 클라이언트 코드 자체가 JdbcMemberRepository
를 사용한다는 것을 알고 있기 때문이다. (new JdbcMemberRepository()
부분)
결국, 이 클라이언트 코드는 어느정도 구현체에 의존하고 있으며, DIP를 위반했다.
이렇게 객체지향과 SOLID에 대해서 정리해보았다.
이 중 가장 중요한 건 OCP와 DIP이다. 이해하기도 어렵고 지키기도 어려운 원칙이다. 어느 정도 이해는 했는데, 이 정도까지 원칙이 빡빡하면 지킬 수는 있는 건가? 그런 의문이 들 정도다.
하지만 다행히도 우리의 선배 개발자 분들께서 이미 오랜 고민 끝에 방법을 마련해 두셨다. 앞으로의 강의가 이 방법론을 배우는 과정이다.
강의를 수강하면서도 헷갈리는 감이 있어 여기저기 찾아보면서 내용을 추가했습니다. 뭔가 이해가 된듯, 안 된듯 그런 느낌? 어느 정도 고민해가면서 열심히 적기는 했지만, 틀린 부분이 있을 수도 있습니다. (특히 추상화 관련해서.)
뭔가 잘못된 부분이 보이면 알려주시면 감사하겠습니다.
References
아주-쉽게-이해하는-OCP-개방-폐쇄-원칙 [Inpa Dev 👨💻:티스토리] - 관련해서 매우 잘 정리된 글입니다. 직접 읽어보시는 것을 매우 추천.
'Programming > Spring' 카테고리의 다른 글
[Spring] 스프링 컨테이너와 스프링 빈 (스프링 핵심원리 - 기본편 section5) (0) | 2025.01.12 |
---|---|
[Spring] 객체 지향 원리 적용하기 (스프링 핵심원리 - 기본편 section4) (0) | 2025.01.11 |
[Spring] 스프링 프레임워크와 스프링 부트 (0) | 2025.01.08 |
[Spring] 김영한 님의 스프링 입문 강의를 수강하고 (0) | 2025.01.01 |
[Spring] ResponseBody Annotation (0) | 2024.05.18 |