1. 단일 책임의 원칙 - Single Responsibility Principle(SRP)
* 하나의 클래스는 하나의 책임만 가져야한다는 원칙
SRP 예시 기존 코드
class User{
private String name;
private String message;
public User(String name, String msg){
this.name = name;
this.message = message;
}
private String getMessage(){
return this.message;
}
private String getName(){
return this.name;
}
}
public class ChatService{
public void getClientName(User user){
System.out.println('client name :' + user.getName());
}
public void sendMessage(User user){
System.out.println('client message :' + user.getMessage());
}
}
클래스 하나에 메시지와 관련된 기능과 사용자에 관련된 기능이 복합적으로 구현되어 있다. 이는 단일 책임 원칙에 어긋나는 구현이다. 이렇게 하나의 클래스에서 여러 가지 복합적인 기능들이 구현되어 있으면 다음에 여러 기능이 추가된다면 유지보수 하기도 어려워지고 가독성도 매우 떨어지게 된다.
SRP 예시 개선 코드
class User{
private String name;
private String message;
public User(String name, String msg){
this.name = name;
this.message = message;
}
private String getMessage(){
return this.message;
}
private String getName(){
return this.name;
}
}
public class ClientService{
public void getClientName(User user){
System.out.println('client name :' + user.getName());
}
}
public class MessageService{
public void sendMessage(User user){
System.out.println('client message :' + user.getName());
}
}
public class ChatService{
ClientService clientService = new ClientService();
MessageService messageService = new MessageService();
public void printClientDetails(User user) {
clientService.getClientName(user);
messageService.sendMessage(user);
}
}
사용자에 관련된 기능을 처리하는 클래스와 메시지에 관련된 기능을 처리하는 클래스로 각각 분리되어서 각 클래스에서 처리하는 기능끼리 구현이 되어있다. 클래스마다 처리하는 기능들이 명확하게 나뉘어 있으며 이렇게 단일 책임 원칙을 지킴으로써 다음에 유지보수 하기도 용이해지고 가독성도 헤치지 않게 된다.
2. 개방 폐쇄의 원칙 - Open Closed Priciple(OCP)
* 클래스가 확장에는 열려있어야 하고 수정에는 닫혀있어야 한다는 원칙
* 원본 코드를 수정하지 않고 새로운 코드를 추가해서 기능들을 확장시켜야 한다.
OCP 예시 기존 코드
public class MessageService{
public String sendMessageBot(Bot bot){
String botMessage = "Bot Message:" + bot.message;
return botMessage;
}
public String sendMessageClient(Client client){
String clientMessage = "Client Message:" + client.message;
return clientMessage;
}
}
해당 클래스에서 새로운 기능을 추가하려면 기존클래스에 함수가 추가돼야 한다. 이는 OCP에 적합하지 않은 방식이고, 기능이나 코드가 길어진다면 가독성이 떨어지고 기존 소스를 변경하게 된다면 기존에 잘되는 기능들에 영향을 끼칠 수 있다.
OCP 예시 개선 코드
public abstract class Message{
public abstract String sendMessage(){}
}
public class Bot extends Message{
@Override
public String sendMessage(String message) {
String botMessage = "Bot Message:" + message;
return botMessage;
}
}
public class Client extends Message{
public String message;
@Override
public String sendMessage(String message) {
String clientMessage = "Client Message:" + message;
return clientMessage;
}
}
OCP 원칙을 지키기 위해 sendMessage라는 함수를 추상 메서드로 선언하고, Bot과 Client 클래스에서 이를 상속받아 구현하게 됩니다. 이렇게 하면 기존 메시지를 보내는 부분을 수정하지 않고, 해당 클래스에서 sendMessage를 재정의하여 기능을 수정하거나 추가할 수 있습니다.
3. 리스코프 치환 원칙 - Listov Substitution Priciple(LSP)
* 파생 클래스가 기존 클래스를 대체할 수 있어야한다는 원칙
즉, 자식 클래스가 부모 클래스의 역할을 대신했을때 원래대로 작동해야한다는 의미이다.
LSP 예시 기존 코드
public abstract class Vehicle{
public void move(){
}
public void turnRight(){
}
public void turnLeft(){
}
}
public class Car extends Vehicle{
@Override
public void move() {}
@Override
public void turnRight() {}
@Override
public void turnLeft() {}
}
public class Train extends Vehicle{
@Override
public void move() {}
@Override
public void turnRight() {}
@Override
public void turnLeft() {}
}
탈것의 클래스를 만들어 놓고 해당 클래스를 자동차와 기차 클래스에서 상속받아 메서드를 재정의해 사용한다. 이때 자동차 클래스에서는 좌회전, 우회전, 이동 메서드를 전부 사용가능하지만 기차 클래스에서는 이동 메서드를 제외한 좌회전 우회전 메서드는 활용할 수 없기때문에 자식클래스에서 부모클래스를 대체할 수 없어서 LSP원칙에 어긋난다.
LSP 예시 개선 코드
// 움직임 인터페이스
public interface Movable {
void move();
}
// 방향 조정 인터페이스
public interface Steerable {
void turnRight();
void turnLeft();
}
public class Car implements Movable, Steerable {
@Override
public void move() {
System.out.println("자동차가 움직입니다.");
}
@Override
public void turnRight() {
System.out.println("자동차가 우회전 합니다.");
}
@Override
public void turnLeft() {
System.out.println("자동차가 좌회전 합니다.");
}
}
public class Train implements Movable {
@Override
public void move() {
System.out.println("기차가 움직입니다.");
}
}
방향조정 관련 메서드를 가진 인터페이스, 움직임 관련된 메서드를 가진 인터페이스 이런식으로 각각 분리해서 자동차 클래스에서는 방향조정, 움직임 인터페이스를 활용해서 관련 메서드를 재정의해 사용하고, 기차 클래스에서는 움직임 인터페이스만 받아서 메서드를 재정의해 각각 클래스에 필요한 기능들만 처리할 수 있게 책임과 역할을 나누어 클래스를 설계할 수 있습니다.
4. 인터페이스 분리 원칙 - Interface Segregation Priciple(ISP)
* 인터페이스를 각각 사용에 맞게 분리해서 설계해야한다는 원칙이다.
ISP 예시 기존 코드
public interface ChatService {
public void sendMessage(String message){
System.out.println("보낸 메시지 :" + message);
};
public void receiveMessage(String message){
System.out.println("받은 메시지 :" + message);
};
public void close(){
System.out.println("채팅방 종료");
}
public void open(){
System.out.println("채팅방 오픈");
}
public void machingForbiddenWord(String inputMessage){
System.out.println("금칙어를 사용 하셨습니다. 금칙어 메시지 : " + inputMessage);
}
}
하나의 인터페이스 안에 메시지를 처리하는 기능, 채팅방 관련된 기능, 금칙어 관련된 기능 등 여러개의 기능들이 다들어가있는 인터페이스이다. 이렇게 여러가지 기능들이 한 곳에 있으면 해당 인터페이스를 클래스에서 활용할 때 쓰지 않는 기능, 쓰는 기능을 분리할 수 없기때문에 결합도가 높아진다.
ISP 예시 개선 코드
public interface ChatRoomService{
public void close(){
System.out.println("채팅방 종료");
}
public void open(){
System.out.println("채팅방 오픈");
}
}
public interface MessageService{
public void sendMessage(String message){
System.out.println("보낸 메시지 :" + message);
};
public void receiveMessage(String message){
System.out.println("받은 메시지 :" + message);
};
}
public interface ForbiddenWordService{
public void machingForbiddenWord(String inputMessage){
System.out.println("금칙어를 사용 하셨습니다. 금칙어 메시지 : " + inputMessage);
}
}
기존 코드에서 기능 처리에 맞게 인터페이스를 분리 시켰다. 이렇게 분리시켜 놓으면 필요한 기능이 있는 인터페이스만 가져다가 활용할 수있고 분리되어 있으니 가독성은 올라가고 결합도는 낮아지게된다. 위의 SRP는 클래스를 분리하고 각 클래스에 책임과 역할이 분명하게 나누는 것이라면 ISP는 인터페이스를 분리하고 각 인터페이스에 책임과 역할을 나누는것이다.
5. 의존 역전 원칙 - Dependency Inversion Priciple(DIP)
* 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다는 원칙
DIP 예시 기존 코드
public class OnOffService{
public void close(){
System.out.println("close");
}
public void open(){
System.out.println("open");
}
}
public class Chat{
private OnOffService onOff;
public OnOffService(OnOffService onOff){
this.onOff = onOff;
}
public void closeChat(){
onOff.close();
}
public void openChat(){
onOff.open();
}
}
채팅 클래스에서 채팅방을 열고 닫는 기능을 구현하는데 열고 닫는 기능을 가지고있는 OnOff 인스턴스를 직접 접근하여 채팅방을 열고 닫는 기능이 구현되어있다. 이런식으로 구현된 기능은 채팅 클래스에서 열고 닫는 기능을 수정하려면 저수준 모듈인 OnOff 클래스까지 직접 수정해야 하므로 고수준 모듈이 저수준 모듈에 의존적이게 된다.
DIP 예시 개선 코드
public interface OnOff {
void open();
void close();
}
public class ChatService implements OnOff{
@Override
public void close() {
System.out.println("채팅방 종료");
}
@Override
public void open() {
System.out.println("채팅방 오픈");
}
}
public class RoomService{
private OnOff onOff;
public RoomService(OnOff onOff){
this.onOff = onOff;
}
public void close(){
onOff.close();
}
public void open(){
onOff.open();
}
}
기존 코드에서는 RoomService 클래스가 ChatService라는 특정 구현에 직접 의존할 가능성이 있습니다. DIP 원칙을 지키기 위해 RoomService가 ChatService 대신 OnOff라는 인터페이스에 의존하게 하여, RoomService가 구체적인 구현이 아닌 인터페이스에만 의존하도록 개선했습니다.
'개발 공부 > Java' 카테고리의 다른 글
| 자바 예외 처리 정리 - Checked, Unchecked, Error (1) | 2025.05.28 |
|---|---|
| [Java] JVM 메모리 구조 (0) | 2024.11.10 |
| 도서 관리 프로그램 (4) | 2022.08.18 |
| GUI 활용해서 계산기 만들기 (0) | 2022.08.08 |
| HashSet 이용한 Lotto 예제 (3) | 2022.07.28 |