본문 바로가기

CS Study

8월 2주차 - 공통 질문

 

 


공통 질문

1. IoC / DI 에 관해 설명하시오
2. 객체 지향 언어의 4가지 특징
3. SOILD 원칙(객체 지향 개발 5대 원리)
4. DTO entity Domain 의 특징

 


 

오늘도.. 꼬리에 꼬리에.. 꼬리에 토론

 

 


1. IoC / DI 에 관해 설명하시오

IoC(제어의 역전)와 DI(의존성 주입)

스프링(Spring) 프레임워크와 같은 의존성 주입 프레임워크에서 많이 사용

 

 

IoC (Inversion of Control) - 제어의 역전

제어의 역전은 프로그램의 흐름을 제어하는 권한이 보통의 애플리케이션 코드에서 프레임워크나 컨테이너로 넘어가는 것을 의미합니다. 이는 전통적인 프로그램 설계와는 반대되는 방식이기 때문에 "역전"이라는 용어가 사용됩니다.

  • 전통적인 설계에서는 객체가 직접 필요한 의존성을 생성하고 제어합니다.
  • IoC를 사용한 설계에서는 객체의 의존성 생성과 제어를 프레임워크나 컨테이너가 담당합니다.

예:전통적인 설계에서는 객체가 다른 객체를 직접 생성하거나 메서드를 호출합니다. 반면, IoC를 사용하면 이러한 제어 흐름이 외부 컨테이너에 의해 관리됩니다.

 

// 전통적인 설계
class Service {
    private Repository repository = new Repository();  // Service가 Repository 객체를 직접 생성
}

// IoC를 사용한 설계
class Service {
    private Repository repository;

    public Service(Repository repository) {  // Repository는 외부에서 주입됨
        this.repository = repository;
    }
}

 

 

DI (Dependency Injection) - 의존성 주입

의존성 주입은 IoC의 구체적인 구현 방법 중 하나로, 객체가 의존하는 다른 객체를 외부에서 주입하는 패턴입니다. DI는 클래스 간의 결합도를 낮추고, 코드의 유연성과 재사용성을 높입니다.

DI는 다음과 같은 방식으로 이루어질 수 있습니다:

 

생성자 주입 (Constructor Injection): 의존성을 생성자를 통해 주입합니다.

class Service {
    private Repository repository;

    // 생성자 주입
    public Service(Repository repository) {
        this.repository = repository;
    }
}

 

세터 주입 (Setter Injection): 세터 메서드를 통해 의존성을 주입합니다.

class Service {
    private Repository repository;

    // 세터 주입
    public void setRepository(Repository repository) {
        this.repository = repository;
    }
}

 

 

인터페이스 주입 (Interface Injection): 의존성 주입을 위한 메서드를 인터페이스로 정의하여 사용합니다. 다만, 이 방식은 잘 사용되지 않습니다.

 

IoC와 DI의 관계

  • IoC는 객체가 자신의 의존성을 스스로 제어하지 않고, 외부에서 제어하도록 하는 개념적인 원칙입니다.
  • DI는 IoC를 구현하는 방법으로, 의존성을 외부에서 주입하여 객체 간의 결합도를 낮추고, 코드의 유연성을 높이는 설계 기법입니다.

장점

  • 유연성: 의존성을 외부에서 주입하기 때문에 코드의 재사용성이 높아집니다.
  • 테스트 용이성: 모킹(mocking) 등을 통해 테스트 시 필요한 의존성을 쉽게 대체할 수 있습니다.
  • 유지보수성: 의존성 관리가 중앙화되어 코드의 유지보수가 용이합니다.

 

예시: 스프링 프레임워크에서의 DI

스프링에서는 @Autowired, @Inject, @Resource와 같은 어노테이션을 사용하여 DI를 구현합니다. 

 

@Component
public class Service {
    private final Repository repository;

    @Autowired  // DI를 통해 Repository가 주입됨
    public Service(Repository repository) {
        this.repository = repository;
    }
}

2. 객체 지향 언어

객체 지향 언어(Object-Oriented Programming, OOP)는 소프트웨어 설계 패러다임

특징

상속, 다형성, 캡슐화, 추상화

 

  • 상속 (Inheritance):
    • 상속은 하나의 클래스가 다른 클래스의 속성과 메서드를 물려받아 사용하는 기능입니다.
    • 예: 동물 클래스를 상속받아 강아지 클래스와 고양이 클래스를 만들 수 있습니다. 이때 강아지와 고양이 클래스는 동물 클래스의 속성과 메서드를 상속받습니다.
  • 다형성 (Polymorphism):
    • 다형성은 같은 메서드가 다른 객체에서 다르게 동작할 수 있도록 하는 특성입니다.
    • 예: 동물 클래스에 소리내기() 메서드가 있다면, 강아지 클래스에서는 '멍멍', 고양이 클래스에서는 '야옹'으로 동작할 수 있습니다.
  • 캡슐화 (Encapsulation):
    • 캡슐화는 객체의 데이터를 보호하고, 외부에서 직접 접근하지 못하도록 하는 기법입니다. 보통 접근 제어자(private, public 등)를 사용해 데이터를 숨기고, 메서드를 통해 접근하도록 합니다.
    • 예: 학생 객체의 이름 속성을 외부에서 직접 변경하지 못하게 하고, setName() 메서드를 통해서만 변경하도록 할 수 있습니다.
  • 추상화 (Abstraction):
    • 추상화는 복잡한 시스템에서 중요한 부분만을 모델링하여 단순화하는 과정입니다. 추상 클래스나 인터페이스를 사용해 추상화할 수 있습니다.
    • 예: 동물이라는 추상 클래스는 공통적인 행동(예: 먹기, 걷기)만 정의하고, 구체적인 동물들은 그 행동을 구체화합니다.

 

장점

  • 재사용성: 클래스와 객체를 재사용하여 코드 중복을 줄일 수 있습니다.
  • 유지보수성: 캡슐화와 모듈화 덕분에 코드의 유지보수와 수정이 용이합니다.
  • 확장성: 상속과 다형성 덕분에 시스템의 기능을 쉽게 확장할 수 있습니다.
  • 유연성: 다형성과 추상화 덕분에 시스템을 더 유연하게 설계할 수 있습니다.

 


 

  • 객체 (Object):
    • 객체는 데이터(속성, 필드)와 메서드(함수, 행동)를 포함하는 독립적인 단위입니다.
    • 예: 학생 객체는 이름과 나이(데이터)를 가지고, 공부하다와 같은 행동(메서드)을 할 수 있습니다.
  • 클래스 (Class):
    • 클래스는 객체를 생성하기 위한 청사진(템플릿)입니다. 객체의 구조와 동작을 정의하는 틀이라고 할 수 있습니다.
    • 예: 학생 클래스는 모든 학생 객체가 가져야 할 속성과 메서드를 정의합니다.

 


3. SOILD 원칙(객체 지향 개발 5대 원리)

SOLID 원칙은 객체 지향 설계의 다섯 가지 기본 원칙을 나타내는 약어입니다. 이 원칙들은 코드의 유연성과 유지보수성을 높이고, 변경에 유연하며 확장 가능한 소프트웨어 시스템을 만드는 데 도움을 줍니다. SOLID 원칙은 로버트 C. 마틴(Robert C. Martin)이 제안한 개념입니다.

SOLID 원칙

  1. Single Responsibility Principle (단일 책임 원칙)
  2. Open/Closed Principle (개방/폐쇄 원칙)
  3. Liskov Substitution Principle (리스코프 치환 원칙)
  4. Interface Segregation Principle (인터페이스 분리 원칙)
  5. Dependency Inversion Principle (의존성 역전 원칙)

1. SRP (Single Responsibility Principle) - 단일 책임 원칙

  • 설명: 클래스는 하나의 책임만 가져야 하며, 클래스는 하나의 변화의 이유만 가져야 합니다.
  • 목표: 클래스를 변경해야 하는 이유가 오직 하나뿐이도록 만듭니다. 각 클래스는 고유의 목적을 가져야 하며, 클래스가 담당하는 기능이 명확해야 합니다.
// 잘못된 예시: 두 가지 책임을 가진 클래스
class UserManager {
    public void createUser() {
        // 사용자 생성 로직
    }

    public void generateReport() {
        // 사용자 보고서 생성 로직
    }
}

// 개선된 예시: 단일 책임으로 분리된 클래스
class UserCreator {
    public void createUser() {
        // 사용자 생성 로직
    }
}

class UserReportGenerator {
    public void generateReport() {
        // 사용자 보고서 생성 로직
    }
}

 

2. OCP (Open/Closed Principle) - 개방/폐쇄 원칙

  • 설명: 소프트웨어 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.
  • 목표: 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 합니다. 기능 추가는 새로운 클래스나 모듈을 통해 이루어져야 합니다.
// 잘못된 예시: 기존 클래스의 수정을 필요로 하는 코드
class Rectangle {
    public int width;
    public int height;
}

class AreaCalculator {
    public int calculateArea(Rectangle rectangle) {
        return rectangle.width * rectangle.height;
    }
}

// 개선된 예시: OCP를 적용하여 기능 확장
interface Shape {
    int calculateArea();
}

class Rectangle implements Shape {
    public int width;
    public int height;

    @Override
    public int calculateArea() {
        return width * height;
    }
}

class Circle implements Shape {
    public int radius;

    @Override
    public int calculateArea() {
        return (int) (Math.PI * radius * radius);
    }
}

 

3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙

  • 설명: 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야 합니다. 자식 클래스가 부모 클래스의 기능을 그대로 유지하면서 확장해야 한다는 의미입니다.
  • 목표: 상속 관계에서 자식 클래스가 부모 클래스의 기능을 깨지 않도록 보장합니다.
// 잘못된 예시: LSP를 위반하는 코드
class Bird {
    public void fly() {
        // 날다
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("타조는 날 수 없습니다.");
    }
}

// 개선된 예시: LSP를 준수하는 코드
class Bird {
    public void move() {
        // 이동 방법 정의
    }
}

class Sparrow extends Bird {
    @Override
    public void move() {
        fly();
    }

    private void fly() {
        // 날다
    }
}

class Ostrich extends Bird {
    @Override
    public void move() {
        run();
    }

    private void run() {
        // 달리다
    }
}

 

 

4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙

  • 설명: 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫습니다. 하나의 큰 인터페이스를 여러 개의 작은 인터페이스로 나눠야 합니다.
  • 목표: 인터페이스가 특정 클라이언트에 특화되도록 분리하여, 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 합니다.
// 잘못된 예시: 너무 많은 기능을 가진 인터페이스
interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    public void work() {
        // 작업 로직
    }

    public void eat() {
        // 로봇은 먹지 않음
    }
}

// 개선된 예시: 인터페이스 분리
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() {
        // 작업 로직
    }

    public void eat() {
        // 식사 로직
    }
}

class RobotWorker implements Workable {
    public void work() {
        // 작업 로직
    }
}

 

5. DIP (Dependency Inversion Principle) - 의존성 역전 원칙

  • 설명: 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화된 인터페이스에 의존해야 합니다.
  • 목표: 상위 모듈과 하위 모듈이 모두 추상화된 인터페이스에 의존하도록 하여, 모듈 간의 결합도를 낮추고 유연성을 높입니다.
// 잘못된 예시: 구체 클래스에 의존하는 코드
class LightSwitch {
    private LightBulb bulb;

    public LightSwitch() {
        this.bulb = new LightBulb();
    }

    public void turnOn() {
        bulb.on();
    }
}

// 개선된 예시: DIP를 적용하여 인터페이스에 의존
interface Switchable {
    void turnOn();
}

class LightBulb implements Switchable {
    public void turnOn() {
        // 전구를 켬
    }
}

class LightSwitch {
    private Switchable device;

    public LightSwitch(Switchable device) {
        this.device = device;
    }

    public void turnOn() {
        device.turnOn();
    }
}

 

SOLID 원칙의 중요성

SOLID 원칙을 준수하면 코드는 더욱 유연해지고, 유지보수하기 쉬우며, 변경에 강한 구조를 가지게 됩니다. 특히 대규모 시스템을 설계할 때, 이 원칙들은 코드의 품질을 높이는 데 중요한 역할을 합니다.

 


4. DTO entity Domain 의 특징

차이점

  • DTO는 데이터 전송을 목적으로 하며, 로직을 포함하지 않는 단순한 구조체입니다.
  • Entity데이터베이스와 직접 매핑되며, 영속성과 식별자를 가지는 객체입니다.
  • Domain은 비즈니스 로직을 포함하는 객체로, 애플리케이션의 핵심 규칙과 행위를 구현합니다.

이 세 가지를 명확히 구분하고 설계하면, 코드의 명확성, 유지보수성, 확장성이 높아집니다.

 

1. DTO (Data Transfer Object)

특징 및 역할:

  • 목적: 데이터 전송을 목적으로 사용됩니다. 주로 한 계층에서 다른 계층으로 데이터를 전송할 때 사용됩니다.
  • 특징:
    • 단순성: DTO는 단순히 데이터를 담는 역할을 하며, 복잡한 비즈니스 로직을 포함하지 않습니다.
    • 직렬화: 네트워크 통신이나 파일 입출력 시 데이터를 쉽게 직렬화할 수 있습니다.
    • 필드: 일반적으로 게터와 세터만을 가지며, 도메인 모델이나 엔티티와 달리 추가적인 로직이 없습니다.
    • 레이어 간 전송: 주로 서비스 계층에서 프레젠테이션 계층으로 데이터를 전달할 때 사용되며, REST API의 요청 및 응답으로 사용되기도 합니다.
public class UserDTO {
    private String name;
    private String email;

    // Getters and Setters
}

 

2. Entity

특징 및 역할:

  • 목적: 데이터베이스와 직접 매핑되는 객체로, 데이터베이스의 테이블 구조를 반영합니다.
  • 특징:
    • 식별자: 각 엔티티는 고유 식별자를 가집니다 (예: id 필드).
    • 퍼시스턴스: 영속성을 가지며, 데이터베이스의 상태를 반영하고, ORM (Object-Relational Mapping) 프레임워크에 의해 관리됩니다.
    • 상태: 데이터베이스의 상태를 나타내며, CRUD (Create, Read, Update, Delete) 연산을 통해 변경됩니다.
    • 종속성: 데이터베이스 스키마에 의존적이며, 데이터베이스와의 매핑에 집중합니다.
@Entity
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    // Getters and Setters
}

 

3. Domain (도메인 모델)

특징 및 역할:

  • 목적: 비즈니스 로직을 담고 있는 객체로, 애플리케이션의 핵심 비즈니스 규칙과 행위를 포함합니다.
  • 특징:
    • 비즈니스 로직: 도메인 객체는 데이터뿐만 아니라 비즈니스 로직을 포함하여 응용 프로그램의 핵심 기능을 나타냅니다.
    • 자율성: 도메인 모델은 스스로 비즈니스 규칙을 지키고, 데이터를 조작할 수 있는 메서드를 포함합니다.
    • 영속성과 독립성: 영속성에 종속되지 않으며, 비즈니스 로직에 집중합니다. 데이터베이스와의 연관보다는 비즈니스 로직 구현에 집중합니다.
public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public void changeEmail(String newEmail) {
        // 비즈니스 로직을 통해 이메일을 변경
        if (isValidEmail(newEmail)) {
            this.email = newEmail;
        }
    }

    private boolean isValidEmail(String email) {
        // 이메일 검증 로직
        return email.contains("@");
    }
}

'CS Study' 카테고리의 다른 글

8월 4주차 GC  (0) 2024.08.19
8월 4주차 JDK, JRE, JVM  (0) 2024.08.19
8월 4주차 jar, war  (0) 2024.08.19
8월 3주차  (0) 2024.08.12
8월 2주차 - 개인 질문  (0) 2024.08.05