본문 바로가기

Spring Introduction/스프링 핵심 원리 - 기본

01. 객체 지향 설계와 스프링 - (4) 좋은 객체 지향 설계의 5가지 원칙(SOLID)

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 핵심 원리 - 기본편에 대해 공부하고, 정리한 내용입니다.

1) SOLID

(1) SOLID란?

 'SOLID'라는 용어는 소프트웨어 엔지니어링의 주요 원칙들을 대표하는 약어로, 이는 로버트 C. 마틴(Robert C. Martin)이 2000년대 초기에 제시한 개념입니다. SOLID는 'Single Responsibility', 'Open-Closed', 'Liskov Substitution', 'Interface Segregation', 'Dependency Inversion'이라는 다섯 가지 핵심 원칙의 각각 첫 번째 글자들을 모아서 만들어진 단어입니다. 

출처: https://velog.io/@haero_kim/SOLID-%EC%9B%90%EC%B9%99-%EC%96%B4%EB%A0%B5%EC%A7%80-%EC%95%8A%EB%8B%A4


(2) SOLID 이전의 소프트웨어 개발 방식

 초기의 소프트웨어 개발 방식은 대부분 절차적 프로그래밍에 기반하였습니다. 이 절차적 프로그래밍에서는, 프로그램의 상태를 변경하는 일련의 명령어가 순차적으로 실행되는 방식을 사용하였습니다.

 

 하지만, 절차적 프로그래밍 방식은 프로그램의 복잡도가 증가함에 따라 코드의 유지 보수가 어려워지는 문제가 있었습니다. 코드의 한 부분을 변경하면, 그 변경이 프로그램의 다른 부분에 어떤 영향을 미칠지 예측하기 어려웠습니다. 이는 프로그램의 크기와 복잡성이 증가할수록 더욱 심각한 문제가 되었습니다.


(3) 객체 지향 프로그래밍의 탄생

 1990년대에 들어서면서, 이런 문제를 해결하기 위해, 객체 지향 프로그래밍(Object-Oriented Programming, OOP)이 널리 사용되기 시작했습니다. OOP데이터와 그 데이터를 조작하는 방법을 '객체'라는 하나의 단위로 묶는 방식을 사용합니다. 이렇게 하면, 프로그램의 각 부분을 독립적으로 설계하고 구현할 수 있게 되어, 코드의 재사용성유지 보수성이 향상됩니다. 그러나, OOP를 사용하는 것만으로는 충분하지 않았습니다.


(4) 객체 지향 프로그래밍의 한계

 하지만, OOP를 사용하더라도 좋은 소프트웨어 설계를 위한 명확한 원칙이 필요했습니다. 왜냐하면, OOP는 '어떻게 프로그래밍을 해야 하는지'에 대한 도구를 제공하지만, '왜' 그렇게 해야 하거나 '어떤 상황에서' 그렇게 해야 하는지에 대한 명확한 지침이 부족했습니다.

 

 즉, OOP는 프로그래밍 방법론을 제시하지만, 그를 어떻게 효과적으로 활용할지에 대한 가이드라인은 부족했습니다.

그래서, OOP를 사용하면서도 '좋은' 소프트웨어를 만들기 위한 분명한 원칙이 필요했습니다. 이런 원칙들은 프로그램의 각 부분이 어떤 일을 해야 하는지, 그리고 그 부분들이 어떻게 서로 연결되어야 하는지에 대한 명확한 규칙을 제공합니다.

 

 예를 들어, 프로그램의 한 부분을 고치면 그 고친 부분이 프로그램의 다른 부분에 어떤 영향을 주는지 예측하기 어렵습니다. 이런 문제는 프로그램이 커지고 복잡해질수록 더욱 심각해집니다. 이런 문제를 줄이려면, 프로그램의 각 부분이 어떤 역할을 해야 하고, 그 부분들이 어떻게 서로 연결되어야 하는지에 대한 명확한 규칙이 필요합니다.


(5) SOLID 원칙의 탄생

※ SOLID (객체 지향 설계) - [ 위키백과 ]
컴퓨터 프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다. SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다. 이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부다.

 

 이런 상황에서 2000년대 초반, 로버트 C. 마틴은 이런 원칙과 기법들 중에서 특히 객체 지향 프로그래밍과 설계에 직접적으로 관련된 핵심 원칙들을 정리하게 됩니다. 그는 이 원칙들이 소프트웨어의 유지보수성, 확장성, 유연성 등을 향상시키는 데 큰 도움이 된다고 주장하였습니다. 그래서 그는 이 원칙들을 'SOLID'라는 단어로 정리하여, 이를 소프트웨어 개발자들에게 널리 알리는 데 주력하였습니다.


 

(6) SOLID의 중요성과 그 영향력

 SOLID 원칙개발자들에게 코드를 어떻게 작성하고 구조화할 것인지에 대한 명확한 지침을 제공합니다. 이 원칙들을 따르면 코드는 더 깔끔하고 이해하기 쉬워지며, 변경과 확장에도 더 잘 대응할 수 있게 됩니다. 더불어, 이 원칙들은 테스트와 디버깅을 더 쉽게 만들어주며, 소프트웨어의 전반적인 품질을 향상시킵니다.

 

 결국, SOLID는 소프트웨어 개발의 복잡성을 다루는 데 중요한 도구로 자리 잡게 되었고, 현재까지도 많은 개발자들이 이 원칙을 지키며, 품질이 높은 소프트웨어를 개발하는 데 이용하고 있습니다. 이러한 이유로 SOLID는 소프트웨어 개발 과정에서 중요한 가치를 지닌 원칙들로 인식되고 있습니다.


(7) SOLID의 의미

약어 의미
SRP 단일 책임 원칙 Single reponsibility principle
OCP 개방-폐쇄 원칙 Open/closed principle
LSP 리스코프 치환 원칙 Liskov substitution principle
ISP 인터페이스 분리 원칙 Interface segregation principle
DIP 의존관계 역전 원칙 Dependency inversion principle

2) 단일 책임 원칙 (SRP)

(1) 단일 책임 원칙의 개념

 단일 책임 원칙(SRP)은 말 그대로 "하나의 클래스는 하나의 책임만 가져야 한다"는 원칙입니다. 그러나 "하나의 책임"이라는 개념은 모호할 수 있습니다. 왜냐하면 책임의 크기는 크거나 작을 수 있으며, 그 정의는 문맥과 상황에 따라 달라질 수 있기 때문입니다.


(2) 변경에 대한 접근

 단일 책임 원칙을 이해하는데 중요한 핵심 요소는 '변경'입니다. 만약 클래스에 변경이 필요할 때, 그 변경이 클래스 전체나 다른 클래스에 큰 영향을 끼치지 않는다면, 그 클래스는 단일 책임 원칙을 잘 따르고 있는 것입니다.

 

 예를 들어, 사용자 인터페이스(UI)의 변경이 비즈니스 로직에 영향을 끼치지 않는다면, 이는 UI와 비즈니스 로직이 잘 분리되어 있음을 나타냅니다. 즉, UI를 담당하는 클래스와 비즈니스 로직을 담당하는 클래스는 각각 다른 '책임'을 가지고 있습니다.

 

 또한, 객체의 생성과 사용이 서로 독립적으로 이루어진다면, 이는 생성과 사용이 각각의 책임을 가지고 있다는 것을 의미합니다. 예를 들어, '사용자 생성'을 담당하는 클래스와 '사용자 정보 조회'를 담당하는 클래스는 각각 다른 책임을 가지고 있습니다.


(3) 단일 책임 원칙의 예제

 예를 들어 사용자 정보를 관리하는 'UserService' 클래스와 게시글 정보를 관리하는 'PostService' 클래스가 있다고 가정해봅시다.

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다. id=" + id));
    }

    // 다른 사용자 관련 메서드들...
}
@Service
public class PostService {
    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public Post findById(Long id) {
        return postRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
    }

    // 다른 게시글 관련 메서드들...
}

 위 코드에서 'UserService' 클래스사용자 정보를 관리하는 책임만을 가지고 있으며, 'PostService' 클래스게시글 정보를 관리하는 책임만을 가지고 있습니다.

 이렇게 각 클래스가 하나의 책임만을 가지도록 설계함으로써 SRP를 준수하게 됩니다. 이렇게 SRP를 적용하면 각 클래스가 하나의 책임만을 가지므로 클래스를 이해하기 쉬워지고, 유지 보수도 용이해집니다. 또한 클래스가 변경되어야 하는 이유가 명확해지므로 코드의 안정성도 높아집니다.


(4) 적절한 책임의 분배

 단일 책임 원칙에 따라 책임을 과도하게 세분화하면 클래스가 너무 많아질 수 있습니다. 이는 코드의 복잡성을 높일 수 있습니다. 반대로, 책임을 한 곳에 몰아넣으면 단일 책임 원칙이 깨질 수 있습니다. 이렇게 되면, 한 클래스의 변경이 다른 클래스에 영향을 미치는 사이드 이펙트가 발생할 수 있습니다.

 예를 들어, '사용자 관리'를 담당하는 한 클래스 '사용자 생성', '사용자 삭제', '사용자 정보 조회', '사용자 정보 수정' 등 모든 책임을 가진다면, '사용자 생성' 로직에 변경이 생겼을 때 '사용자 정보 조회' 로직에도 영향을 끼칠 가능성이 높아집니다. 이러한 문제를 방지하기 위해 적절한 책임의 분배가 필요합니다.

 따라서, 적절한 책임의 분배가 중요합니다. 이는 설계의 핵심이며, 이를 통해 변경이 있을 때 파급 효과를 최소화하고, 코드의 유지 보수성을 높일 수 있습니다.


3) 개방 - 폐쇄 원칙 (OCP)

(1) 개방 - 폐쇄 원칙의 개념

 개방-폐쇄 원칙 "소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다"는 원칙입니다. 이는 기존의 코드를 변경하지 않고도 새로운 기능을 추가하거나 기존 기능을 수정할 수 있어야 함을 의미합니다.

 

이 원칙은 이미 작성된 코드는 그대로 두고 새로운 기능은 새로운 코드를 작성하여 추가하는 것입니다.

예를 들어, 레고 블록을 생각해보면 이해가 쉽습니다. 우리는 이미 만들어진 레고 구조물을 그대로 두고, 새로운 블록을 추가해 구조물을 확장하기도 합니다. 이런 개념이 바로 OCP 입니다.


(2) 개방 - 폐쇄 원칙의 예제

 예를 들어 인터페이스를 활용하면, 기존 코드는 그대로 두고 인터페이스를 구현한 새로운 클래스를 통해 새로운 기능을 추가할 수 있습니다.

// MemberRepository 인터페이스
public interface MemberRepository {
    void save(Member member);
    Member findById(String memberId);
}
// MemberRepository 인터페이스를 구현한 MemoryMemberRepository 클래스
public class MemoryMemberRepository implements MemberRepository {
    public void save(Member member) {
        // 구현 코드...
    }

    public Member findById(String memberId) {
        // 구현 코드...
    }
}
// MemberRepository 인터페이스를 구현한 JDBCMemberRepository 클래스
public class JDBCMemberRepository implements MemberRepository {
    public void save(Member member) {
        // 새로운 구현 코드...
    }

    public Member findById(String memberId) {
        // 새로운 구현 코드...
    }
}

 

 위의 코드에서 `MemberRepository` 인터페이스는 기존 코드를 변경하지 않고 새로운 기능을 확장하는 데 사용됩니다.

`MemoryMemberRepository` 클래스`JDBCMemberRepository` 클래스는 `MemberRepository` 인터페이스를 구현하여 기능을 확장합니다.

public class MemberService
{
    //private MemberRepository mr = new MemoryMemberRepository();
    private MemberRepository mr = new JDBCMemberRepository();
}

 이렇게 `MemberService` 클래스에서 `MemberRepository`의 구현체를 직접 변경할 수 있습니다. 이 방식을 사용하면, `MemberService` 클래스의 구현 내용을 변경하지 않고도, `MemberRepository`의 구현체를 쉽게 변경할 수 있습니다. 

이것이 바로 개방-폐쇄 원칙(OCP)의 장점이며, 이를 통해 코드의 유연성 확장성을 높일 수 있습니다.


(3) 개방 - 폐쇄 원칙 예제의 문제점

 하지만, 이 방식에는 한 가지 문제가 있습니다.
`MemberService` 클래스에서 `MemberRepository`의 구현체를 변경하려면 `MemberService`의 코드를 변경해야 합니다.
즉, 구현 객체를 변경하려면 클라이언트 코드를 변경해야 합니다. 이렇게 변경을 하게되면 다형성은 사용하였지만, OCP 원칙을 지킬 수가 없습니다.

=> 이 문제점은 추후 스프링을 공부하면서 해결할 수 있습니다. ['의존성 주입(Dependency Injection)']


4) 리스코프 치환 원칙 (LSP)

(1) 리스코프 치환 원칙의 개념

  리스코프 치환 원칙"하위 클래스는 그것의 상위 클래스를 대체할 수 있어야 한다"입니다.

이를 좀 더 쉽게 풀어보자면, 어떤 클래스를 상속받아 기능을 확장하든, 수정하든 그 결과로 만들어진 하위 클래스는 상위 클래스의 역할을 그대로 수행할 수 있어야 한다는 것입니다.


(2) 원칙의 중요성

 컴파일러 차원에서는 상위 클래스의 메서드를 오버라이드하는 것이 허용되지만, 이 원칙에 따르면 그것만으로 충분하지 않습니다. 하위 클래스가 상위 클래스의 역할을 완벽히 수행할 수 있어야 하므로, 내부 로직을 어떻게 구현하든 그 결과는 상위 클래스의 기능과 동일해야 합니다.


(3) 리스코프 치환 원칙의 예제

 예를 들어 "자동차"라는 상위 클래스가 있고, 그것의 메서드로 "pressAccelerator(엑셀을 밟다)"가 있다고 가정해봅시다. 이 메서드는 "자동차"를 앞으로 가게 하는 기능을 합니다.

public class Car {
    public void pressAccelerator() {
        System.out.println("자동차가 앞으로 갑니다.");
    }
}

 

이제 이 "자동차" 클래스를 상속받아 "레이싱카"라는 하위 클래스를 만들어보겠습니다. 

public class RacingCar extends Car {
    @Override
    public void pressAccelerator() {
        System.out.println("레이싱카가 빠르게 앞으로 갑니다.");
    }
}

 여기서 'RacingCar' 클래스는 'Car' 클래스의 'pressAccelerator' 메서드를 오버라이드하여 빠르게 앞으로 가는 기능을 구현하였습니다.

이 경우에는 LSP 원칙이 지켜집니다. 왜냐하면 '레이싱카'도 '자동차'이므로 앞으로 가는 기능을 수행할 수 있고, 빠르게 가는 것은 '레이싱카'의 특성에 맞기 때문입니다. 

하지만 만약 "레이싱카" 클래스에서 "엑셀을 밟다" 메서드를 오버라이드하여 뒤로 가게 만들면, 이는 LSP를 위반한 것입니다.

public class RacingCar extends Car {
    @Override
    public void pressAccelerator() {
        System.out.println("레이싱카가 뒤로 갑니다.");
    }
}

 

 이 경우 '레이싱카'는 '자동차'의 역할, 즉 '엑셀을 밟다'라는 기능을 제대로 수행하지 못하게 됩니다.

따라서 이 경우는 LSP 원칙을 위반한 것입니다. 이렇게 LSP를 위반하면, 상위 클래스를 참조하여 프로그래밍을 하는 개발자는 예상하지 못한 결과를 얻게 되어 프로그램의 안정성이 떨어지게 됩니다.

 결국, 이 원칙은 인터페이스를 구현한 모든 구현체를 믿고 사용하려면, 예상 가능한 동작을 해야 한다는 것을 요구합니다. 이렇게 되면 개발자는 상위 클래스만을 보고 프로그래밍을 할 수 있으며, 이는 코드의 유지 보수성을 크게 향상시킵니다.


5) 인터페이스 분리 원칙(ISP)

(1) 인터페이스 분리 원칙의 개념

 인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 객체 지향 설계 원칙 중 하나로, "특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다"는 원칙을 말합니다.


(2) 인터페이스 분리 원칙의 이해

 이 원칙은 클라이언트가 필요하지 않는 메서드에 의존하지 않도록, 필요한 기능만을 제공하는 인터페이스를 사용하도록 권장합니다. 즉, 큰 범용 인터페이스보다는 필요한 기능에 집중한 작은 인터페이스를 사용하는 것이 더 좋다는 의미입니다.


(3) 인터페이스 분리 원칙의 예제

 예를 들어, '자동차' 인터페이스가 있고 이 인터페이스에는 '운전''정비'라는 두 가지 기능이 있다고 가정해봅시다.

public interface Car {
    void drive();
    void repair();
}

 

 이런 경우, '운전자' 클라이언트와 '정비사' 클라이언트는 '자동차' 인터페이스를 사용하게 됩니다. 하지만 '운전자'는 '정비' 기능을, '정비사'는 '운전' 기능을 사용하지 않습니다.

 

 따라서 이 두 기능을 '운전' 인터페이스와 '정비' 인터페이스로 분리하면, 각각의 클라이언트는 필요한 기능만 사용할 수 있게 됩니다.

public interface Driving {
    void drive();
}

public interface Repairing {
    void repair();
}
public class Driver implements Driving {
    @Override
    public void drive() {
        System.out.println("운전을 시작합니다.");
    }
}

public class Mechanic implements Repairing {
    @Override
    public void repair() {
        System.out.println("차량을 수리합니다.");
    }
}

 

이렇게 인터페이스를 분리하면, '운전자'는 '운전' 기능만 사용하고, '정비사'는 '정비' 기능만 사용하게 됩니다. 이는 인터페이스 분리 원칙(ISP)을 잘 따르는 예시입니다.

 

인터페이스가 작아지면 구현이 쉬워져서 다른 구현체로 대체하기도 쉬워집니다. 뿐만 아니라, 한 인터페이스의 변화가 다른 인터페이스에 영향을 끼치지 않으므로 코드의 유지 보수성이 향상됩니다. 따라서 이 원칙은 소프트웨어의 품질을 높이는데 중요한 역할을 합니다.


6) 의존관계 역전 원칙 (DIP)

(1) 의존관계 역전 원칙의 개념

 의존관계 역전 원칙(Dependency Inversion Principle, DIP)은 객체 지향 설계의 핵심 원칙 중 하나로서, "추상화에 의존해야 하며, 구체화에 의존하면 안 된다"라는 주요 원칙을 내포하고 있습니다. 이 원칙은 소프트웨어 설계에 있어 중요한 역할을 담당하며, 효과적인 프로그래밍을 위한 가이드라인을 제공합니다.


(2) 의존관계 역전 원칙의 이해

 이 원칙이 주장하는 바는 고차원의 모듈이 저차원의 모듈에 의존하지 않도록, 둘 다 추상화에 의존하도록 설계해야 한다는 것입니다. 이는 구현 클래스에 의존하는 것이 아니라 인터페이스에 의존하도록 코드를 작성해야 한다는 의미를 내포하고 있습니다. 여기서 '인터페이스'라는 개념은 특정 기능을 수행하는 방법을 정의한 것을 말하며, 이를 통해 다양한 구현 방법을 가능하게 합니다.


(3) 의존관계 역전 원칙의 예제

 이 원칙을 쉽게 이해하기 위해, 실생활에서의 예시를 들어보겠습니다. 운전자는 자동차의 역할에 의존해야 하며, K3나 테슬라 같은 특정 모델에 의존하면 안 됩니다. 즉, 운전자는 자동차가 가지는 기본적인 기능, 즉 '운전'이라는 추상화된 개념에 의존해야 합니다.

 

 마찬가지로, 로미오를 연기하는 배우는 줄리엣의 대본에 의존해야 하며, 줄리엣을 연기하는 특정 배우가 바뀌었다고 해서 연기를 할 수 없게 되면 안 됩니다.

 

 이렇게 원칙을 지키면, 클라이언트는 인터페이스에 의존하기 때문에 유연하게 구현체를 변경할 수 있습니다. 이는 개발 과정에서의 유연성을 증가시키며, 코드의 변경이나 확장을 용이하게 만들어 줍니다. 반면, 구현체에 의존하게 되면 변동이 발생했을 때 변경이 어렵게 됩니다.


(4) 의존관계 역전 원칙의 코드예제

 아래의 `MemberService` 클래스는 `MemberRepository` 인터페이스에 의존하면서 동시에 `MemoryMemberRepository`나 `JDBCMemberRepository` 같은 구현 클래스에도 의존하고 있습니다.

public class MemberService {
    //private MemberRepository mr = new MemoryMemberRepository();
    private MemberRepository mr = new JDBCMemberRepository();
}

 

 이와 같이 코드를 작성하면, `MemberService`는 `MemberRepository`의 구현체를 직접 선택하게 되므로 DIP를 위반하게 됩니다. 이는 확장이나 변경이 필요할 때 클라이언트 코드를 변경해야 하므로 개방-폐쇄 원칙(Open-Closed Principle, OCP)도 위반하게 됩니다.

 

 결국 원칙의 적용 따라서, DIP를 지키기 위해서는 구현체가 아닌 인터페이스에 의존하는 코드를 작성해야 합니다. 이를 통해 코드의 유연성과 확장성을 높일 수 있습니다.


7) 내용 정리

(1) 객체 지향의 핵심은 다형성이다.

  • 다형성은 상속, 인터페이스, 추상화 등을 통해 구현됩니다.
  • 같은 메소드 호출에 대해 각 객체가 서로 다르게 반응하도록 만들어, 유연성과 확장성을 제공합니다.

 

(2) 다형성만으로는 쉽게 부품을 갈아 끼우듯 개발할 수는 없다.

  • 다형성은 동일한 인터페이스를 공유하는 객체들 간의 상호 작용을 가능케 하지만, 그 자체로는 객체 간의 의존성을 관리하지 못합니다.
  • 특정 구현체를 교체하려면, 그 구현체를 직접 참조하고 있는 모든 코드를 찾아 변경해야 합니다.

 

(3) 다형성만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.

  • 클라이언트 코드가 특정 구현 클래스에 의존하고 있을 경우, 해당 클래스의 구현이 변경되면 클라이언트 코드도 수정해야 합니다.
  • 이는 강한 결합도를 초래하며, 유지 보수와 확장을 어렵게 만듭니다.

 

(4) 다형성 만으로는 OCP, DIP를 지킬 수 없다.

  • OCP(Open-Closed Principle)는 기존의 코드를 변경하지 않고(닫혀 있음) 기능을 추가하거나 변경할 수 있도록(열려 있음) 설계해야 한다는 원칙입니다. 다형성만으로는 이를 충족시키기 어렵습니다.
  • DIP(Dependency Inversion Principle)는 상위 수준의 모듈이 하위 수준의 모듈에 의존하는 것이 아니라, 둘 다 추상화에 의존해야 한다는 원칙입니다. 다형성은 이 원칙을 부분적으로 지원하지만, 완전히 지키기 위해서는 추가적인 설계 기법이 필요합니다.

 

(5) 뭔가 더 필요하다.

  • 이 '뭔가'는 바로 '의존성 주입(Dependency Injection)'이라는 기법입니다.
  • 이를 통해 구현 객체와 클라이언트 코드 사이의 의존성을 외부에서 관리할 수 있게 되어, 코드의 유연성과 확장성을 크게 향상시킬 수 있습니다.

7) Spring과 객체 지향은 무슨 관계인가?

(1) 스프링과 객체 지향

  • 스프링은 객체 지향 프로그래밍의 핵심 원칙들인 개방-폐쇄 원칙(OCP)과 의존관계 역전 원칙(DIP)을 구현하는 데 도움을 줍니다.
  • 이 원칙들은 다형성만으로는 충족하기 어렵지만, 스프링을 통해 쉽게 구현할 수 있습니다.

 

(2) 의존성 주입(Dependency Injection, DI)

  • 스프링은 의존성 주입을 통해 객체 간의 의존 관계를 관리합니다.
  • 이를 위해 스프링은 DI 컨테이너를 제공하며, 이 컨테이너에 등록된 객체들로 의존성을 주입합니다.

 

(3) 기능 확장

  • 스프링의 DI 기능을 활용하면, 클라이언트 코드의 변경 없이 기능을 확장할 수 있습니다.
  • 이를 통해 OCP를 지키면서 프로그래밍을 할 수 있습니다.

 

(4) 스프링과 객체 지향 개발

  • OCP와 DIP를 지키면서 순수한 자바로 객체 지향 개발을 진행하다 보면, 결국 스프링과 유사한 형태의 프레임워크가 만들어집니다. 예를 들어, DI 컨테이너를 구현하게 될 것입니다.
  • 이는 스프링이 객체 지향 원칙을 잘 따르는 프레임워크라는 것을 입증합니다.

8) 내용 정리

(1) 역할과 구현의 분리

  • 모든 설계에서 역할과 구현을 분리해야 합니다.
  • 위에서 예를 들었던, 자동차나 공연을 떠올리면 이해하기 쉽습니다.
  • 이렇게 역할과 구현을 분리함으로써, 구현체가 바뀌더라도 역할을 수행하는 클라이언트 코드에는 영향을 미치지 않습니다.

 

(2) 다형성과 SOLID 원칙

  • 이러한 구현의 유연성을 가능하게 하는 핵심 원리는 다형성입니다.
  • 또한, 객체 지향 설계 원칙인 SOLID(단일 책임 원칙, 개방-폐쇄 원칙, 리스코프 치환 원칙, 인터페이스 분리 원칙, 의존관계 역전 원칙) 중에서 특히 개방-폐쇄 원칙(OCP)과 의존관계 역전 원칙(DIP)를 잘 활용하고 지켜야 합니다.

 

(3) 인터페이스의 사용

  • 이상적으로는 모든 설계에 인터페이스를 부여하는 것이 좋습니다. 이렇게 하면 구현체와 클라이언트 코드 사이의 결합도를 낮추고, 시스템의 유연성을 높일 수 있습니다)

9) 실무에서의 고민 

- 인터페이스를 가능한 한 많이 적용하려 하면, 그로 인한 추상화에 대한 비용이 발생합니다.

 

(1) 추상화에 따른 비용

  • 인터페이스를 사용하면 코드를 통해 직접적으로 구현체를 알 수 없게 됩니다.
  • 이는 코드를 읽는 데에 추가적인 시간이 필요하게 만들며, 이는 일종의 비용으로 볼 수 있습니다.

 

(2) 적절한 인터페이스의 적용

  • 기능 확장의 가능성이 없는 경우에는 구현 클래스를 직접 사용하는 것이 효율적일 수 있습니다.
  • 이후에 기능 확장이 필요하게 될 때 리팩토링을 통해 인터페이스를 도입하는 것이 바람직합니다. 따라서, 인터페이스의 적용은 항상 비용과 효율성을 고려하여 결정해야 합니다.