실제 코드에 적용된 OOP 설계 원칙을 깊이 있게 살펴보기
객체지향 설계 원칙 중 가장 널리 알려진 것이 바로 SOLID이다.
하지만 이 원칙들은 단순한 이론으로만 공부하면 이해하기 어렵고,
실제 코드 안에서 어떻게 나타나는지 경험해 봐야 비로소 깊이 있게 체감할 수 있다.
이번 글에서는 SNS(Post, Comment, User) 도메인을 객체지향적으로 설계한 실제 코드를 기반으로
SOLID 원칙이 어떻게 적용되었는지 하나씩 심층 분석해본다.
단순히 “적용되었다” 수준이 아니라, 왜 그렇게 설계되었는지, 어떤 지점이 특히 훌륭한지, 개선 여지는 무엇인지까지 함께 다룬다.
1. SRP — 단일 책임 원칙
클래스는 오직 하나의 책임만 가져야 한다
SRP(Single Responsibility Principle)는
객체지향 설계에서는 가장 기본적이면서 가장 중요한 원칙이다.
하나의 클래스가 너무 많은 역할을 책임지면 재사용성도 떨어지고
변경에 취약한 구조가 되기 때문이다.
PositiveIntegerCounter
public class PositiveIntegerCounter {
private int count;
public void increment() { this.count++; }
public void decrement() {
if (this.count <= 0) {
return;
}
this.count--;
}
}
이 클래스는 “양수 기반 카운터”라는 하나의 역할만 가진다.
- count가 음수로 내려가지 않도록 보호
- increment/decrement 기능만 제공
- Post, Comment, User가 사용하는 공통 로직을 하나로 집중
이 구조 덕분에 좋아요 수, 팔로워 수, 팔로잉 수 등
여러 도메인에서 중복되는 카운팅 로직을 각자 구현할 필요가 없다.
단일 책임을 훌륭하게 지킨 예다.
DatetimeInfo
public class DatetimeInfo {
private boolean isEdited;
private LocalDateTime dateTime;
public void updateEditDateTime() { ... }
}
작성/수정 시각 관리라는 하나의 역할만 담당한다.
Post나 Comment가 시간 관리 책임을 떠안지 않아도 된다.
Content / PostContent / CommentContent
Content는 텍스트 관리,
PostContent와 CommentContent는 길이 검증 로직만 담당한다.
각 클래스의 책임이 명확하게 분리되어 있어 SRP의 좋은 사례이다.
2. OCP — 개방/폐쇄 원칙
확장에는 열려 있고, 변경에는 닫혀 있어야 한다
OCP(Open/Closed Principle)는
“기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다”는 원칙이다.
이 도메인에서 가장 잘 드러나는 부분은 Content 계층이다.
Content 계층
public abstract class Content { ... }
public class PostContent extends Content { ... }
public class CommentContent extends Content { ... }
새로운 콘텐츠 타입이 생긴다면Content를 수정할 필요 없이 새로운 하위 클래스를 만들기만 하면 된다.
예를 들어 DMContent, ArticleContent 등도 쉽게 추가할 수 있으며
상위 구조는 수정할 필요가 없다.
PositiveIntegerCounter의 재사용성
좋아요, 팔로워, 팔로잉 등 다양한 카운트에 재사용할 수 있도록
공통 로직을 분리해 OCP를 실천한 사례이다.
현재 구조의 약한 OCP 지점
public void like(User user) {
if (this.author.equals(user)) {
throw new IllegalArgumentException("User cannot like their own post");
}
likeCounter.increment();
}
좋아요 규칙이 추가되면 Post 내부 코드를 계속 수정해야 한다.
- 차단한 사용자 처리
- 특정 권한 사용자만 좋아요 허용
- 하루 1회만 좋아요 가능 등의 정책
이런 확장에는 정책 객체(LikerPolicy 등)를 도입하면 OCP를 더 강화할 수 있다.
3. LSP — 리스코프 치환 원칙
부모 타입 대신 자식 타입을 사용해도 이상이 없어야 한다
LSP의 대표적인 적용 사례는 Content 상속 구조이다.
Content c1 = new PostContent("hello world");
Content c2 = new CommentContent("hi");
- Content 타입으로 사용하더라도
PostContent/CommentContent가 문제없이 동작한다. - updateContentText(), getContentText() 같은 기능을 일관되게 제공한다.
- 예외 규칙, 유효성 검사 방식이 동일한 철학을 따른다.
추상화가 LSP를 자연스럽게 지키고 있다.
4. ISP — 인터페이스 분리 원칙
사용하지 않는 기능에 의존하게 만들어선 안 된다
이 코드에는 명시적인 인터페이스가 등장하지 않지만
각 클래스가 최소한의 공개 메서드만 제공하여
자연스럽게 ISP를 따르고 있다.
예를 들어 User는 follow(), unfollow() 외의 다른 기능을 노출하지 않는다.
public class User {
public void follow(User targetUser) { ... }
public void unfollow(User targetUser) { ... }
}
Post 역시 like(), dislike(), updatePost()만 제공한다.
불필요한 기능을 클래스 외부에 노출하지 않는다.
규모가 커진다면 Likeable, EditableContent 인터페이스로
분리하는 확장도 고려할 수 있다.
5. DIP — 의존 역전 원칙
상위 수준 모듈은 추상화에 의존해야 한다
도메인 모델에서 DIP 적용 여부를 판단할 때 중요한 기준은 다음이다.
- 도메인 객체가 인프라(DB, 프레임워크)에 의존하지 않는가?
- 구체 타입이 아닌 추상화에 의존할 수 있는 구조인가?
순수 도메인 모델 구조
현재 Post, Comment, User 등은
어떤 프레임워크에도 의존하지 않는다.
- @Entity, @Id 같은 JPA 어노테이션 없음
- DB나 외부 서비스 호출 없음
- POJO(Pure Java Object) 상태 그대로 유지
도메인 모델이 외부 기술에 오염되지 않아
DIP 철학을 잘 따른 구조다.
Content 계층은 DIP 적용의 좋은 사례
Content는 추상 클래스이며
PostContent/CommentContent는 구체 구현이다.
Post나 Comment는 Content라는 “추상화”에 의존한다.
결론: 이 도메인 모델은 SOLID 원칙을 잘 반영한 실제 예제이다
정리하면, 이 SNS 도메인 모델은 다음 특징을 갖는다.
- SRP: 책임 분리가 잘 되어 있으며, Value Object 구조가 강점
- OCP: Content 확장을 통해 변화 최소화를 실현
- LSP: Content 계층이 안전하게 치환 가능
- ISP: 클래스마다 최소한의 기능만 외부에 제공
- DIP: 외부 인프라에 의존하지 않는 순수한 도메인 구조 유지
특히 Content 계층, PositiveIntegerCounter, DatetimeInfo는
SOLID를 가장 잘 보여주는 부분이다.
실무에서도 적용 가능한 도메인 모델이며, 추후 JPA, Service Layer, Repository 패턴으로 확장하더라도 핵심 객체지향 설계 원칙이 흔들리지 않는다.
답글 남기기