스프링 핵심원리 정리

Seung jun Cha·2022년 6월 15일
0

1. 좋은 객체지향 설계의 원칙

  • 객체지향 프로그래밍(OOP): 현실 세계를 프로그래밍 설계에 반영한다는 개념을 기반으로 한다. 현실의 사물이 가지고 있는 속성과 기능을 참고해서 클래스를 만들고, 클래스를 기반으로 객체를 만든다. 객체지향 프로그래밍은 그 객체들 간의 유기적인 상호 작용을 통해 로직을 구성하는 프로그래밍 기법이다.
    객체 단위로 코드가 나누어져 있기 때문에 디버깅이 쉽고 유지보수에 용이하다. 객체와 매핑하는 것이 수월하기 때문에 요구사항을 보다 명확하게 파악하여 프로그래밍 할 수 있다.

  • 단점

    • 복잡성 (Complexity):
      객체지향 프로그래밍은 추상화, 상속, 다형성 등의 개념을 도입하여 복잡성이 증가할 수 있습니다. 객체 간의 상호작용을 모델링하기 위해 추가적인 코드 작성이 필요할 수 있습니다.
    • 성능 (Performance):
      객체지향 프로그래밍은 유연성과 추상화를 위해 일부 성능 손실이 발생할 수 있습니다. 또한 객체를 생성하고 관리하는 과정에서 메모리 사용량도 증가할 수 있습니다.
    • 설계 비용 (Design Overhead):
      클래스와 관련된 상속 구조, 인터페이스 정의, 객체 간의 관계 등을 고려해야 하므로 설계 단계에서 더 많은 시간과 노력을 필요로 합니다.

1-1. SRP 단일책임의 원칙

  • 하나의 클래스는 하나의 액터에 대한 책임만 가져야 한다.
    액터는 객체의 기능을 이용하는 주체자를 말한다.
    책임이란, 하나의 특정 액터(actor)를 위한 기능 집합이다. 책임이 메서드를 의미하지는 않는다
    즉, 클래스는 특정한 집단을 위한 기능만을 가져야한다.
  1. 먼저 요구사항을 분석해 액터를 정의한다.
  2. 각 액터에게 제공해야 할 책임을 파악한다.
  3. 함수와 클래스 각각이 단 하나의 책임만 할당받도록 함수, 클래스를 그룹화한다.
잘못된 코드 

public class Animal {

    private String animal;

    public void setAnimal(String animal) {
        this.animal = animal;
    }

    public void cry () {

        if(animal == "Dog") { // 강아지
            System.out.println("bark!");
        }
        else if(animal == "Cat") { // 고양이
            System.out.println("meow..");
        }
    }

}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
abstract class Animal {
    abstract void cry();
}

class Dog extends Animal {
    @Override
    void cry() {
        System.out.println("bark!!!");
    }
}

class Cat extends Animal {
    @Override
    void cry() {
        System.out.println("Meow...");
    }
}

1-2. OCP 개방-폐쇄의 원칙

  • 기능을 확장하거나 변경할 때 기존의 코드를 변경하지 않아야한다.
  • 인터페이스를 활용, 다형성을 활용(역할과 구현을 분리), DI, IOC컨테이너 활용
  • 객체가 공통적으로 사용하는 기능을 인터페이스로 분리하고 각자 상속한다. 그리고 각 객체의 기능은 부모클래스인 인터페이스로 활용

아래 코드는 개방-폐쇄원칙을 준수했다. 각각의 다른 카드도 모두 Purchasable를 구현했으므로 다 묶을 수 있다.

public interface Purchasable
{
    boolean send(int price);
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
class CardA implements Purchasable
{
  
    @Override
    public boolean send(int price)
    {
        ....
    }
}

class CardB implements Purchasable
{
   
    @Override
    public boolean send(int price)
    {
      ...
    }
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
public class Pos
{
  
    public boolean purchase(Purchasable purchasable, int price)
    {
        return purchasable.send(price);
    }
}

1-3. LSP 리스코프 원칙

  • 부모객체를 호출하는 동작에서 자식객체가 부모객체를 완전히 대체할 수 있다. 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 원칙이다. 부모 객체의 동작을 완벽하게 대체할 수 있는 관계만 상속하도록 코드를 설계해야한다.
    ex) 정사각형 객체가 직사각형 클래스를 상속하고 넓이를 구했을 때

public class Square extends Rectangle
{

    @Override
    public void setWidth(int width)
    {
        super.setWidth(width);
        super.setHeight(getWidth());
    }
    

    @Override
    public void setHeight(int height)
    {
        super.setHeight(height);
        super.setWidth(getHeight());
    }
}

Rectangle rectangle = new Rectangle();
        rectangle.setWidth(10);
        rectangle.setHeight(5);

1-4. ISP 인터페이스 분리 원칙

  • 모든 기능을 모아놓은 범용 인터페이스가 아닌 기능별로 적절히 분리한 인터페이스를 설계, 기능 각각을 별도의 인터페이스로 만든다.
    기능이 명확해지고 대체가능성이 높아진다
    ex) 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스
public class S20 extends SmartPhone(공통적 기능만 들어간 스마트폰 클래스)
implements WirelessChargable, ARable, Biometricsable

1-5. DIP 의존관계 역전 원칙

  • 구현체가 클래스가 아닌 인터페이스에 의존할 것
    (코드를 안다, 해당 코드가 쓰이고 있다)

    ex) private MemberRepository MemberRepository m = new MemoryMemberRepository();
    DIP위반 => 생성자 주입을 사용할 것(외부에서 객체를 생성해서 생성자로 주입)

  • DI(의존관계 주입)
    애플리케이션 실행시점에 외부에서 객체 생성 -> 클라이언트에 전달
    -> 클라이언트와 서버의 의존관계가 연결되는 것


public interface Attackable
{
  
    int attack();
    
    @Override
    String toString();
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
public class OneHandSword implements Attackable
{
    private final String NAME;
    private final int DAMAGE;

    public OneHandSword(String name, int damage)
    {
        NAME = name;
        DAMAGE = damage;
    }
    
    @Override
    public int attack()
    {
        return DAMAGE + new Random().nextInt(10) - 5;
    }
    ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
    public class Character
{
    private final String NAME;
    private int health;
    private Attackable weapon;
    

    public Character(String name, int health, Attackable weapon)
    {
        NAME = name;
        this.health = health;
        this.weapon = weapon;
    }
  
    public int attack()
    {
        return weapon.attack();
    }
    이러면 캐릭터는 모든 무기를 다룰 수 있다.
    

2. 스프링

2-1. 스프링 컨테이너

(1)스프링 컨테이너 생성

  1. new AnnotationConfigApplicationContext(AppConfig.class)
    설정정보를 파라미터로 주고 스프링 컨테이너 생성한다.

  2. xml에서 ApplicationContext를 생성하기 위해서는 <listener>
    <context-param>이 필요하다.

 <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
  1. 빈으로 등록된 객체들은 스프링 컨테이너인 ApplicationContext 내에서 생성되고 관리된다.

(2)스프링 빈 등록, 의존관계 설정

  • Bean이란 스프링 컨테이너가 생성하고 관리하는 애플리케이션의 주요 요소들을 말한다. 컨테이너가 Bean의 라이프사이클을 관리하며 필요에 따라 인스턴스화하고 의존성을 주입한다.

  • @Bean이 붙은 메서드 명을 Bean의 이름으로 사용하고 반환된 객체는 Bean객체로 등록되어 관리된다. (Bean의 이름은 모두 달라야 한다)'
    클래스의 경우 클래스 명이 빈의 이름으로 등록된다, 그리고 등록된 Bean 끼리 의존관계를 설정한다.

2-2. 싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하고, 애플리케이션 실행시점에 외부에서 객체 생성하고 의존성을 주입하는 디자인 패턴이다. DI를 사용하면 객체의 생성 및 생명 주기를 외부에서 관리할 수 있습니다. 이로써 객체의 생성과 소멸을 더 유연하게 다룰 수 있습니다.
    (클래스 A가 클래스 B에 의존한다면, 클래스 A에 대한 제어권은 클래스 B에게 넘어가게 됩니다. 이때 클래스 A는 클래스 B를 외부 의존성으로 간주합니다.)

  • 외부에서 new를 통해 생성하지 못하도록 막아야 한다.
    스프링 컨테이너를 사용하면 싱글톤 패턴을 적용하지 않아도 빈을 싱글톤으로 관리해줌
    즉, 스프링 컨테이너에 등록된 모든 빈은 싱글톤

(1) 싱글톤 방식의 주의점

  • 싱글톤 객체는 stateless로 설계해야 한다.
    1) 특정 클라이언트에 의존적인 필드가 있으면 안된다
    2) 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다
    3) 가급적 읽기만 가능해야한다(수정을 피해야 한다.)
    4) 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

  • @Configuration과 CGLIB
    설정클래스도 Bean으로 등록이 되는데 해당 클래스가 바로 Bean으로 등록되는 것이 아니다. 스프링이 바이트코드 조작라이브러리를 사용해 해당 클래스를 그대로 상속받은 CGLIB클래스를 만들어 스프링 Bean으로 등록한다. 그래서 싱글톤을 확실히 보장 받을 수 있다.

(2) protoType

  • 작업할 때마다 빈을 새로 생성하고 싶은 경우로 싱글톤과는 다르다.

(3) request

  • http 요청때마다 빈을 새로 생성하고 싶은 경우

2-3. 컴포넌트 스캔, 의존관계 주입

2-3-1 Component Scan

  • config에 설정
@ComponentScan
@Configuration(basePackages = {"org.example.overview"}) 
// String으로 등록하기 때문에 오류 발생가능성이 있다

@ComponentScan(basePackageClasses = {LoginController.class})
// class를 지정하여 해당 클래스가 위치한 곳에서부터 모든 어노테이션을 스캔함

@ComponentScan(basePackages = "org.example.overview", 
useDefaultFilters = false) // useDefaultFilters : 해당 패키지 경로에서는 
bean을 등록하지 않는다

2-3-2 의존성 주입

  1. 설정클래스에 @Bean을 사용해 개발자가 수동으로 빈을 등록하는 것이 아닌
    @Bean이나 @Conpoment가 사용된 것들을 스프링이 자동으로 읽어서 빈으로 등록하고 싱글톤으로 관리
    1-1 @Component : 클래스 자체를 빈으로 등록
    1-2 @Bean : 메서드 레벨에서 사용하며 메서드의 결과로 리턴되는 객체를 빈으로 등록

  2. 동시에 의존관계 설정도 필요하므로 생성자 주입(의존성 = 사용한다) Controller - Service - Repository만 빈으로 등록하는 것이 아닌 싱글톤 빈으로 만들어서 객체로 사용할 것들은 그 외 @Bean 또는 @Component을 사용해서 등록해준다
    (타입으로 빈을 조회해서 주입)

  3. @SpringBootApplication 안에 @ComponentScan이 들어가 있음

  4. 자동 빈 등록과 수동 빈 등록으로 빈의 이름이 중복되면 오류 발생

  5. IOC : 개발자 입장에서 제어권이 사라졌다는 것을 표현한다. 객체의 생성, 의존성 관리와 코드의 최종 호출은 개발자가 제어하는 것이 아닌 스프링 내부에서 결정된 대로 이루어진다.
    DI : 스프링이 애플리케이션 실행에 필요한 의존성을 주입해주는 것으로 클래스 사이의 의존 관계를 빈(Bean) 설정 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것을 말한다.

    (1) 생성자 주입

  • 의존성을 객체 생성 시점에 외부에서 주입하는 방식으로 해당 객체의 인스턴스가 생성되는 시점에서 필수적으로 의존성이 제공되어야 한다.

  • 클래스에 생성자가 1개만 있으면 @Autowired를 생략해도 의존성 주입이 이루어짐

  • 대부분의 의존관계를 한 번 설정하면 변경할 일이 거의 없는데 setter주입을 사용하면 setter메서드를 public으로 열어두어야하므로 실수로 변경할 수 있다. 하지만, 생성자는 객체를 생성할 때 딱 한 번 호출되고 이후에 호출되지 않으므로 불변으로 설계가능
    생성자의 매개변수로 필요한 의존성을 선언하므로, 코드를 보고 쉽게 클래스가 어떤 의존성을 사용하는지 파악할 수 있습니다.

@Service
public class A {
		public final B b;
		public final C c;
		public final D d;
		
		@Autowired
		public A(B b, C c, D d) {
			this.b = b;
			this.c = c;
			this.d = d;
		}
        // A객체가 생성되기 위해서 B,C,D 객체가 모두 필요
        // B,C,D -> A
        (순환참조를 막을 수 있다, nullpointEexp을 막는다)
}

`
- 자바코드로 테스트 할 때, 생성자에 주입 데이터 누락 시 컴파일 오류발생
- final키워드 사용이 가능하여 생성자에서 값이 설정되지 않는 경우를 방지할 수 있음
=> 그냥 @RequiredArgumemtConstructor를 쓰면 됨

(2) 중복타입의 bean 주입

  • @RequiredArgumemtConstructor는 생성자에 @Autowired가 생략된 것이고, @Autowired는 빈을 등록할 때, 같은 타입의 Bean이 있는지 조회한다. 같은 타입의 Bean이 2개 이상이면 Bean을 주입할 때 문제가 발생한다.

    1.@Autowired 필드명 : 타입이 같은 빈이 두 개 이상이면 필드명 또는 파라미터명이 같은 빈을 주입 , 스프링 프레임워크에서만 사용가능하다.
    1-1 @Autowired(required = false) : 빈이 null값을 가질 수 있음?

    2.@Qualifier("...") : @Qualifier의 이름과 같은 빈을 매칭
    (빈 자체의 이름을 바꾸는 것은 아님), 주로 생성자 주입을 사용하기 때문에 잘 사용하지는 않는다.

    3.@Primary : @Primary가 선언된 빈이 우선순위를 가짐

@Repository
public class SampleDaoImpl implements SampleDao{
}

@Repository
@Primary
public class EvenSampleDaoImpl implements SampleDao{
}

@Service
public class SampleService {
    
    @Autowired
    private SampleDao sampleDao;
    //SampleDao타입의 빈이 2개인데 @Primary가 있는 빈이 우선 적용됨
}

2-4 bean 라이프사이클

  • 빈의 라이프사이클은 주로 초기화 및 정리 작업에 사용된다.
  1. 스프링 컨테이너 생성 : 스프링 애플리케이션을 구동할 때, 스프링 컨테이너가 생성됩니다. 스프링 컨테이너는 빈의 생성, 관리, 주입 등을 담당하는 중요한 역할을 수행합니다.

  2. 스프링 빈 생성 : 스프링 컨테이너는 설정된 정보에 따라 스프링 빈들을 생성합니다. 빈은 일반적으로 자바 클래스로 정의되며, 스프링 컨테이너에 의해 객체화됩니다.

  3. 의존관계 주입 : 빈들 간의 의존성을 주입하는 작업을 수행합니다. 스프링 컨테이너는 빈들의 의존성을 해결하여 객체들을 조립합니다.

  4. 초기화 콜백사용 @PostConstruct : 빈이 생성된 후에 자동으로 호출되는 메서드를 의미하며, 이를 활용하여 빈이 사용 가능한 상태로 만들어지도록 할 수 있습니다. 데이터베이스 연결, 외부 리소스 로딩, 설정 초기화 등의 작업이 이에 해당합니다.

     @PostConstruct
       public void init() {
           // 빈 초기화 작업 수행
       }
  1. 빈 사용

  2. 소멸전 콜백 @PreDestroy : 빈이 소멸되기 전에 자동으로 호출되는 메서드를 의미하며, 빈이 종료될 때 필요한 작업을 수행할 수 있도록 합니다. 데이터베이스 연결을 닫거나 외부 리소스를 반환해야 할 때 소멸 전 콜백이 사용됩니다.

    @PreDestroy
       public void cleanup() {
           // 빈 소멸 전 작업 수행
       }

callback이란 bean을 생성, 초기화, 파괴하는 등 특정 시점에 호출하도록 정의된 함수를 말한다

2-5 Bean Property 설정

  • @Value : SpEL로 properties의 메타 정보를 가져와서 설정하거나 평가식을 만들어서 그 결과의 값으로 기본 값을 초기화
    properties 값을 가지고 올때는 $ 사용
1. @Value("#{1 + 1}")
int a;

2. @Value("#{2 eq 2}")
boolean a;

3. @Value("${admin.name})
String name;

4. @Value(#{ ${admin.name}  eq 'cha' }")
boolean a;

3 캡상추다클인

  1. 캡슐화 : 접근 제어자, Getter와 Setter, 패키지 분리 등으로 객체의 속성과 기능을 감추는 것을 말한다. 객체가 외부와 상호작용하기 위해 공개한 인터페이스와 메서드를 통해서만 데이터에 접근할 수 있다. 데이터에 접근하는 것을 막으면, 신뢰할 수 있는 코드를 작성하고 데이터의 무결성을 유지할 수 있다.

  2. 상속 : 상위클래스를 기반으로 하위클래스를 생성하는 것을 말한다.

  3. 추상화 : 객체들의 속성과 기능 중에 공통적인 부분을 추출해서 인터페이스 같은 상위 수준의 클래스로 정의하는 것을 말한다. 이렇게 되면 코드의 복잡성이 줄어들고 구현체를 추가하거나 수정해도 인터페이스는 수정할 필요가 없어서 유연성이 높아진다.

  4. 다형성 : 하나의 객체 또는 메서드가 여러 타입을 참조할 수 있다.

  • 객체의 다형성 : 자식객체가 부모 객체의 인스턴스로 할당될 수 있다.
  TV tv2 = new SmartTV();
  이 경우, SmartTV에 선언된 메서드들 중 TV에 선언된 메서드와 일치하는 메서드만 
  사용 가능하다.
 
 - 객체뿐만아니라 인터페이스를 상속할때도 가능하다

  interface Movable
{
    void move(boolean direction);
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
class Unit implements Movable
{
    @Override
    public void move(boolean direction)
    {
        // 동작
    }
    public void work(String act)
    {
        // 동작
    }
}

public class Main
{
    public static void main(String[] args)
    {
        Movable movable = new Unit();  // 다형성 적용
        
        // Movable에 존재하는 메소드이므로 호출 가능(동작은 Unit메서드로 동작한다)
        movable.move(true);
        // Movable엔 없는 Unit만의 고유 메소드이므로 호출 불가능
        movable.work("run");
    }
}

대표적으로 오버라이딩과 오버로딩, 부모타입으로 자식타입을 대체하는 경우가 있다.
5. 클래스 : 객체의 속성과 기능을 정의하고 있다.
6. 인스턴스 : 클래스를 기반으로 만들어진 객체가 메모리에 할당되면 인스턴스이다.

4 스프링

4-1 특징

  • 자바 기반의 오픈 소스 프레임워크로, IOC, DI AOP, 트랜잭션 관리, 테스트 지원, MVC구조 등을 지원한다.
  • 스프링 부트스프링의 복잡한 설정이나 환경 구성에 시간을 낭비하지 않고, 최소한의 설정으로 애플리케이션을 만들 수 있게 돕는 도구이다. 애플리케이션에 필요한 기본적인 설정을 자동으로 수행하고, 내장된 서버를 사용하여 애플리케이션을 빠르고 간편하게 실행할 수 있다.
  1. AOP : 로깅, 트랜잭션 관리, 보안 등 여러 클래스에서 공통적으로 사용되는 부가기능을 분리하여 모듈화하는 프로그래밍 접근 방식이다. 코드의 재사용성을 높이고 유지보수를 용이하게 한다.

  2. 트랜잭션 : 데이터의 일관성과 무결성을 보장하기 위해 데이터베이스의 상태를 변경하는 작업을 하나의 단위로 묶은 것을 말한다.
    여기서 일관성이란 데이터베이스의 모든 제약 조건과 규칙을 준수하고 있는 상태를 말한다.
    무결성이란 무결성 규칙과 제약 조건을 준수하고 있는 상태를 말한다. 무결성 규칙은 개체 무결성, 참조 무결성 등이 있다.
    트랜잭션은 ACID라는 다음의 특징을 가진다.
    (1) 원자성 : 트랜잭션으로 묶인 작업은 하나의 단위로 처리되며 트랜잭션 내의 작업이 하나라도 실패하면 실패하게 된다. 만약 트랜잭션 도중에 오류가 발생하면 이전 상태로 롤백됩니다.
    (2) 일관성 : 트랜잭션이 실행되기 전과 실행된 후의 데이터베이스 상태는 일관된 상태여야 합니다.
    (3) 고립성 : 서로 다른 트랜잭션은 간섭없이 독립적으로 실행되어야 합니다. 한 트랜잭션이 진행 중인 데이터에 다른 트랜잭션이 접근할 수 없습니다.
    (4) 지속성 : 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 유지되어야 합니다. 시스템 장애 또는 기타 문제가 발생해도 트랜잭션의 결과는 유지되어야 합니다.

0개의 댓글