기술 면접 준비

[기술면접] Spring - 3/3

Lea Hwang 2023. 3. 2. 17:11

[기술면접] Spring - 3/3의 목차

  • SOLID란?
  • 디자인패턴
  • MVC패턴
  • 선언형, 명령형 프로그래밍 (+ 프로그래밍 패러다임)
  • 객체지향 프로그래밍
  • 절차형 프로그래밍

 

 


 

 

😎 SOLID란?

객체지향 프로그래밍 5가지 설계 원칙, SOLID

SOLID란 객체 지향 프로그래밍을 하면서 지켜야하는 5대 원칙으로 각각

SRP(단일 책임 원칙),

OCP(개방-폐쇄 원칙),

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

DIP(의존 역전 원칙),

ISP(인터페이스 분리 원칙)의 앞글자를 따서 만들어졌다.

SOLID 원칙을 철저히 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 되는 것으로 알려져있다.

 

 

[ 단일 책임의 원칙(SRP, Single Responsibility Principle) ]

어떤 클래스가 단 하나의 책임 만을 가지고 있어야한다.

SRP는 하나의 모듈이 하나의 책임을 가져야 한다는 모호한 원칙으로 해석하면 안된다. 대신 모듈이 변경되는 이유가 한가지여야 함으로 받아들여야 한다. 여기서 변경의 이유가 한가지라는 것은 해당 모듈이 여러 대상 또는 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야 한다는 것을 의미한다.

만약 어떤 모듈이 여러 액터에 대해 책임을 가지고 있다면 여러 액터들로부터 변경에 대한 요구가 올 수 있으므로, 해당 모듈을 수정해야 하는 이유 역시 여러 개가 될 수 있다.

 

장점:

단일 책임 원칙을 제대로 지키면 변경이 필요할 때 수정할 대상이 명확해진다. 

시스템이 커질수록 극대화되는데, 시스템이 커지면서 서로 많은 의존성을 갖게되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문이다.

 

 

[ 개방 폐쇄 원칙 (Open-Closed Principle, OCP) ]

개방 폐쇄 원칙(Open-Closed Principle, OCP)은 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙으로, 각각이 갖는 의미는 다음과 같다.

  • 확장에 대해 열려 있다: 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.

 

[ 인터페이스 분리 원칙 (Interface segregation principle, ISP) ]

객체가 충분히 높은 응집도의 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리해줄 필요가 있는데, 이를 인터페이스 분리 원칙이라고 부른다. 즉, 인터페이스 분리 원칙이란 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이다.

 

인터페이스 분리 원칙을 준수함으로써 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스(외부에서 접근 가능한 메세지)만을 접근하여 불필요한 간섭을 최소화할 수 있으며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있다.

 

인터페이스 분리 원칙을 지킨다는 것은 어떤 구현체에 부가 기능이 필요하다면 이 인터페이스를 구현하는 다른 인터페이스를 만들어서 해결할 수 있다. 예를 들어 파일 읽기/쓰기 기능을 갖는 구현 클래스가 있는데 어떤 클라이언트는 읽기 작업 만을 필요로 한다면 별도의 읽기 인터페이스를 만들어 제공해주는 것이다.

 

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더욱 세밀하게 제어할 수 있다. 그리고 이렇게 인터페이스를 클라이언트의 기대에 따라 분리하여 변경에 의해 의한 영향을 제어하는 것을 인터페이스 분리 원칙이라고 부른다.

 

 

[ 리스코프 치환 원칙 (Liskov Substitution Principle, LSP) ]

올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 하위 타입은 상위 타입을 대체할 수 있어야 한다는 것이다. 즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다.

 

 

[ 의존 역전 원칙 (Dependency Inversion Principle, DIP) ]

의존 역전 원칙이란 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것이다. 객체 지향 프로그래밍에서는 객체들 사이에 메세지를 주고 받기 위해 의존성이 생기는데, 의존성 역전의 원칙은 올바른 의존 관계를 위한 원칙에 해당된다. 여기서 각각 고수준 모듈과 저수준 모듈이란 다음을 의미한다.

  • 고수준 모듈: 입력과 출력으로부터 먼(비즈니스와 관련된) 추상화된 모듈
  • 저수준 모듈: 입력과 출력으로부터 가까운(HTTP, 데이터베이스, 캐시 등과 관련된) 구현 모듈

 

의존 역전 원칙이란 결국 비즈니스와 관련된 부분이 세부 사항에는 의존하지 않는 설계 원칙을 의미한다.

 

의존 역전 원칙은 개방 폐쇄 원칙과 밀접한 관련이 있으며, 의존 역전 원칙이 위배되면 개방 폐쇄 원칙 역시 위배될 가능성이 높다. 또한 의존 역전 원칙에서 주의해야 하는 것이 있는데, 의존 역전 원칙에서 의존성이 역전되는 시점은 컴파일 시점이라는 것이다. 

 

 

정리

SOLID가 얘기하는 핵심은 결국 추상화와 다형성이다. 구체 클래스에 의존하지 않고 추상 클래스(또는 인터페이스)에 의존함으로써 우리는 유연하고 확장가능한 애플리케이션을 만들 수 있는 것이다.

 

 

 

 

😎 디자인 패턴 

일종의 설계 기법이며, 설계 방법이다. 디자인 패턴은 특정한 구현이 아닌 아이디어이다.

프로젝트에 적용한다면 추후 재사용, 호환, 유지 보수시 발생할 수 있는 문제를 예방하기 위해 패턴을 만들어 둔 것이다. 

 

 

목적

재사용성, 호환성, 유지 보수성

 

원칙 : SOLID(객체지향 설계 원칙)

1. SRP(단일 책임 원칙)

하나의 클래스는 반드시 하나의 책임만을 가져야한다. 

 

2. OCP(개방-폐쇄 원칙)

확장에는 열려있고 수정에는 닫혀있어야함

 

3. LSP(리스코프 치환 원칙)

자식이 부모의 자리에 항상 교체될 수 있어야함, 부모 객체 대신 자식 객체를 넣어도 기능이 동작해야한다.

 

4. DIP(의존 역전 원칙)

상위 모듈이 하위 모듈에 의존하면 안 됨, 추상화에 의존해야지, 구체화에 의존하면 안된다.

 

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

하나의 일반적인(general) 인터페이스보다, 여러 개의 구체적인 인터페이스를 만들어야한다. 

 

 

⭐ 분류

3가지 패턴의 목적을 이해해야함

 

1. 생성 패턴 :  객체의 생성 방식을 결정

예) DBConnection을 관리하는 Instance를 하나만 만들 수 있도록 제한하여, 불필요한 연결을 막음.

  • 팩토리 패턴: 객체를 생성하기 위한 디자인 패턴
  • 추상 팩토리 패턴: 객체를 생성하는 팩토리를 생성하기 위한 디자인 패턴
  • 빌더 패턴: 상황에 따라 동적인 인자를 필요로 하는 객체를 생성하기 위한 디자인 패턴
  • 싱글톤 패턴: 객체를 1개만 생성하여 항상 참조가능하도록 고안된 디자인 패턴

 

2. 구조 패턴 : 객체간의 관계를 조직

예) 2개의 인터페이스가 서로 호환이 되지 않을 때, 둘을 연결해주기 위해서 새로운 클래스를 만들어서 연결시킬 수 있도록 함.

  • 어댑터 패턴: 호환성이 맞지 않는 두 클래스를 연결하여 사용하기 위한 디자인 패턴
  • 프록시 패턴: 어떤 객체에 접근 제어를 위해 대리인을 사용하는 디자인 패턴
  • 데코레이터 패턴: 어떤 객체에 새로운 기능 추가를 위해 대리인을 사용하는 디자인 패턴
  • 퍼사드 패턴: 어떤 복합적인 기능에 대해 간략화된 인터페이스를 제공하는 디자인 패턴

 

3. 행위 패턴 : 객체의 행위를 관리

예) 하위 클래스에서 구현해야 하는 함수 및 알고리즘들을 미리 선언하여, 상속시 이를 필수로 구현하도록 함.

  • 전략 패턴: 상황에 따라 다른 전략을 사용하기 위한 디자인 패턴
  • 옵저버 패턴: 값을 관찰하여 빠르게 반영하기 위한 디자인 패턴
  • 커맨드 패턴: 실행될 기능을 캡슐화하여 재사용성이 높은 클래스를 설계하기 위한 디자인 패턴

 

 

싱글톤 패턴

애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴

 

  • 정의: 하나의 객체는 하나의 인스턴스만 가진다.
  • 객체를 미리 생성해 두고, 필요할 때 해당 객체를 가져다 쓴다
  • 장점:
    • 메모리 관리의 효율성 증가
    • 클래스 간의 데이터 공유가 쉽다
  • 단점:
    • 싱글톤 구현을 위한 코드 자체가 많이 필요하다
    • 테스트가 어렵다 (단위 테스트, TDD)
    • 클라이언트가 구체 클래스에 의존함

 

 

팩토리 패턴

  • 객체 생성과 사용 부분을 떼어 내어 별도의 객체로써 생성하고 사용하는 것
  • 상위 클래스에서는 중요한 뼈대를 결정하고, 하위 클래스에서는 객체 생성에 관한 구체적인 내용을 결정
  • 예시: 상위 클래스에서는 커피에 필요한 필수요소만 가지고, 하위 클래스에서는 각 커피 종류에 대한 구체적인 내용이 결정됨
  • 장점:
    • 객체 간에 결합도를 낮출 수 있다 (느슨한 결합을 가진다).
    • 단일 책임 원칙 (Single Responsibility Principle)을 따른다.
    • 개방 폐쇄 원칙 (Open/Closed Principle)을 따른다.
    • 위 장점을 종합하여 유지 보수 상 유리함을 가진다.
  • 단점:
    • 패턴을 구현할 많은 서브 클래스를 구현함으로써 코드 양이 많아질 수 있다.

 

 

전략 패턴

  • 특정 객체의 행위를 직접 구현하지 않고, 캡슐화된 객체로 구현하여, 컨텍스트 내에서 상호 교체가 가능하도록 만드는 패턴
  • 예: 온라인 상에서 물건 구매를 위한 결제를 진행할 때, 결제 수단은 여러 가지가 존재하고, 그 중 어떤 것을 선택해도 성공적으로 결제가 진행된다.
  • 행위의 추가/삭제에 용이하며, 기존 로직을 건드리지 않는다.

 

 

옵저버 패턴

  • 상태 변화를 감지하는 관찰자 역할을 하는 객체가 존재하며, 변화가 발생하면 옵저버 목록에 있는 옵저버들에게 변화를 알려 주는 디자인 패턴
  • 특정 변화에 따른 동작이 미리 정의되어 있고, 변화가 발생하면 이러한 동작이 즉각 수행된다.
  • 이벤트 처리를 매우 효율적으로 구현할 수 있다.
  • 프록시 패턴이 적용되어 있는 디자인 패턴

 

 

 

 

😎 MVC패턴

Model, View, Controller로 애플리케이션의 구성 요소를 구분하여, 각각의 기능에 집중하는 디자인 패턴이다.

Model, View, Controller는 각각 무엇을 뜻할까? 주로 아래와 같은 기준으로 구분된다.

  • Model: 애플리케이션을 구성하는 정보. DB 등의 데이터 계층을 나타냄
    • 데이터와 관련된 책임을 가진다
    • 비지니스 로직을 처리한다
  • View: 사용자에게 보여지는 화면, UI를 나타냄
    • Model이 처리한 데이터를 받아서 합산한다
    • 데이터, 로직은 없어야 한다
  • Controller: Model과 View를 연결해 주는 중개자 레이어
    • 사용자의 요청에 맞는 서비스를 실행 (Model에서 진행됨)
    • Model에서 처리한 값을 View에 전달
    • Spring의 Controller, Service, Dao, Dto, Repository는 이 계층에 해당
  • Controller를 제외한 View, Model은 다른 레이어를 알아서는 안 된다.
  • 장점:
    • 역할의 분리, 단일 책임 원칙에 따르도록 개발할 수 있다.
    • FE, BE 개발자가 각각 따로 개발할 수 있다.
  • 단점:
    • Model과 View 사이의 의존성이 높아진다. 높은 의존성은 애플리케이션이 커질수록 유지보수를 어렵게 한다.

 

 

 

😎 선언형, 명령형 프로그래밍 (+ 프로그래밍 패러다임)

프로그래밍 패러다임

프로그래머에게 프로그래밍 관점을 갖게 해주는 개발 방법론. 아래와 같은 패러다임이 존재한다.

  • 객체지향 프로그래밍
  • 함수형 프로그래밍
  • 절차형 프로그래밍
  • 등등...

 

크게 선언형, 명령형으로 나뉘며, 선언형은 함수형 등으로 세분화된다. 명령형은 객체지향, 절차지향 등으로 세분화된다.

  • 선언형
    • 함수형...
  • 명령형
    • 객체지향
    • 절자지향 ...

 

 

선언형 프로그래밍

선언형 프로그래밍 (declarative programming)이란, 무엇을 풀어내는가에 집중하는 패러다임이다.

함수형 프로그래밍은 선언형 프로그래밍의 일종이다.

 

함수형 프로그래밍은, 이와 같이 작은 '순수 함수'들을 블록처럼 쌓아 로직을 구현하고, '고차 함수'를 통해 재사용성을 높인 프로그래밍 패러다임이다. 1.8 이후의 Java, Kotlin, JavaScript 등이 함수형 프로그래밍을 지원한다.

 

순수함수
출력이 입력에만 의존하는 함수를 의미한다. 아래의 `getMax()` 메소드 역시 순수 함수이다.

public class Calc {

    public int getMax(List<int> num) {

        int result = 0;

        for (int data : num) {
            result = Math.max(result, data);
        }

        return result;
    }
}

 

 

고차함수

함수가 함수를 값처럼 매개변수로 받아 로직을 생성할 수 있는 것을 뜻한다.

 

 

 

 

😎 객체지향 프로그래밍

OOP (Object-Oriented Programming)은, 애플리케이션을 객체들의 집합으로 본다. 여기서 객체란, 실제 세계에 존재하는 개념을 코드로 나타낸 것이다. 설계에 많은 시간이 소요되며, 처리 속도가 다른 프로그래밍 패러다임에 비해 상대적으로 느리다.

public class Calc {

    private int maxVal;

    public Calc(List<int> num) {

        this.maxVal = 0;

        for (int data : num) {
            this.maxVal = Math.max(this.maxVal, data);
        }
    }

    public int getMaxVal() {
        return this.maxVal;
    }
}

 

위 코드에는 객체지향 프로그래밍의 여러 가지 내용이 포함되어 있다.

  • maxVal은 private이다. 따라서 외부에서는 maxVal에 직접 접근할 수 없다.
  • Calc라는 클래스가 호출될 때, 생성자는 파라미터로 받은 숫자 리스트 `num` 중에서 최댓값을 찾고, 그 값을 maxVal에 할당한다.
  • maxVal의 값은 메소드 `getMaxVal()`을 통해서만 접근할 수 있다.

 

객체지향 프로그래밍의 특징

  • 추상화 (abstraction)
  • 캡슐화 (encapsulation)
  • 상속성 (inheritance)
  • 다형성 (polymorphism)

 

 

추상화

특정 개념을 구체화 시키지 않고 모호화 하는 것으로 구현체를 쉽게 교체할 수 있다는 장점이 있다.

public interface Pet {
    public void walk();
}

public class Dog implements Pet {

    private String name;

    @Override
    public void walk() {
        // 대충 개를 산책시키는 방법
    }
}

public class Cat implements Pet {

    private String name;

    @Override
    public void walk() {
        // 대충 고양이를 산책시키는 방법
    }
}
  • 개를 키울지, 고양이를 키울지는 못 정했지만, 애완동물은 키우기로 정했다면, 먼저 인터페이스 (애완동물을 키울 때 산책을 시킬 것)를 정의하고, 실제 구현체 (개 또는 고양이) 는 추후에 구현하면 된다.
  • 또는, 구현체를 언제든지 쉽게 교체할 수 있다.

 

 

캡슐화

객체의 속성과 메소드를 하나의 클래스로 묶고, 필요한 부분 외에는 외부에 노출하지 않는 것을 의미한다.

  • 마치 리모콘과 같은데, 필요한 기능은 버튼으로 외부에 노출되어 있고, 그 외에 기판 등 밖에 보여질 필요가 없는 내용은 숨겨져 있다.

 

상속성

상위 (부모) 클래스의 특성을 하위 (자식) 클래스가 이어받아서 재사용하는 것이다.

public class Animal {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

public class Cat extends Animal {

    private String color;
    private int age;

    // 이하 getter, setter가 있다고 가정
}
  • 위 코드에서, '모든 동물은 이름이 있다' 라는 전제 하에, `Animal` 클래스는 모든 동물의 기본 사항만을 포함함
  • 그 외에 고양이에게만 있는 내용은 색깔과 나이라고 가정하고, `Cat` 클래스는 `Animal` 클래스에는 없는 변수와 메소드를 가짐
  • 그럼에도 고양이 역시 `name`과 set, get 메소드를 사용할 수 있음

 

다형성

같은 이름을 가진 메소드가 여러 개 존재할 수 있는 것을 의미

크게 오버로딩, 오버라이딩이 있다.

 

- 오버로딩 : 매개변수에 따라 같은 이름을 가진 함수가 구분되는 것

public class Calculator {

    public int calc (int num) {
        return num*num;
    }

    public int calc (int num1, int num2) {
        return num1 * num2;
    }
}

// calc(3) = 9
// calc(3, 4) = 12

 

- 오버라이딩 : 자식 클래스가 부모 클래스의 메소드를 재정의하는 것으로 런타임 중에 발생함

public class Animal {

    private String name;

    public void cry() {
        printf(name + " 응애");
    }
}

public class Cat extends Animal {

    @Override
    public void cry() {
        printf(name + " 야옹");
    }
}

 

 

 

 

😎 절차형 프로그래밍

수행되어야 할 로직을 따라 코드가 작성되는 패러다임

  • 수행해야 하는 일을 순차적으로 작성하면 되므로, 코드를 작성하기에 빠르며 실행 속도도 빠르다.
  • 따라서 계산이 많은 작업 등에 유리하다 
  • 모듈화하기 어렵고, 유지 보수성이 떨어진다

 

 

 

 

 

 

 

 

 

관련 포스팅

[기술면접] Spring - 1/3
[기술면접] Spring - 2/3
[기술면접] Spring - 3/3

 


 

 

 

출처 : 

https://mangkyu.tistory.com/95
https://dev-coco.tistory.com/163
https://gyoogle.dev/blog/