KKanging

[Spring] 스프링 기초, 구조 분석 본문

백엔드/Spring framework

[Spring] 스프링 기초, 구조 분석

천방지축 개발자 2024. 5. 6. 13:26

 

스프링 기초, 구조 분석과 객체 지향

벡엔드란?

#클라이언트가 필요로 하는 데이터를 관리, 응답 , 처리를 하는 분야

- 데이터 관리는 DB를 통해서 관리하고
- 응답은 클라이언트가 필요로하는 데이터를 가공해서 응답한다.
- 처리는 비지니스 적인 로직을 이용하여 처리한다.
- 웹,앱에 대해 성능 개선을 위한 설계하는 것도 백엔드 엔지니어의 역할이다.

스프링 프레임워크는 이를 더욱 효과적이고 안전한 개발을 하도록 지원해주는 
자바 기반 백엔드 프레임워크다.

스프링이란?

스프링 역사

스프링 이전에 EJB라는 예전 기술이 존재 하였다.

다만 EJB는 개발자가 다루기 어렵고 지나치게 EJB 의존적이라는 이유로 사용하기 어려웠다.

유겐 휠러라는 사람이 로드 존슨의 책을 보고 스프링이라는 프레임워크를 오픈소스로 개발

<스프링은>
JAVA 의 웹 프레임워크로 JAVA 언어를 기반으로 사용한다. 
JAVA 로 다양한 어플리케이션을 만들기 위한 프로그래밍 툴이다.

(스프링의 역사에 대해 더 자세히 알고 싶은 인원은 따로 검색해도 좋을 것)

 

스프링 생태계는 스프링 기본 프레임워크에 다양한 기능을 지원하는 프레임워크가 부가적으로 존재한다.

 

스프링의 기능

핵심 기술 : 스프링 DI 컨테이너, AOP, 이벤트, 기타
웹 기술 : 스프링 mvc, 스프링 webFlux
데이터 접근 기술 : 트랜잭션, jdbc, orm xml 지원
기술 통합 : 캐시, 이메일, 원격접근, 스케줄링
테스트 : 스프링 기반 테스트 지원
언어 : 코틀린, 그루비
#최근에는 스프링 부트를 통해서 스프링 프레임 워크의 기술들을 편리하게 사용한다.

스프링 부트의 기능

- 위에 스프링의 불편한 점을 해결하기 위해 스프링 부트가 등장하였다.

- Tomcat 같은 웹서버를 내장하여 별도의 웹서버를 설치하지 않아도 되고

- starter 를 구성하여 하나의 starter를 빌드하면 알아서 필요한 라이브러리를 묶어서 빌드한다.

- 그리고 버전에 맞는 검사까지 해주는 등 손쉽게 스프링을 사용할 수 있게 해준다.

- 단, 스프링 부트는 스프링을 손쉽게 해주는 도구이지 스프링을 대체하는 것이 아니다.
 (하지만 스프링 부트를 설치하면 알아서 스프링을 가져온다)

 

💡 스프링 부트는 스프링은 손쉽게 해주는 도구일 뿐이지만, 지원해주는 기능이 강력한 만큼

요즘은 필수로 사용한다.

관용적으로 스프링부트 스프링을 같이 스프링으로 부른다.

(둘 다 필수적이라 구분하지 않는다는 의미)

 

스프링의 핵심 개념


스프링의 핵심 개념은

위의 스프링이 지원해주는 강력한 기능들이 아닌

다음과 같다.

자바 기반의 프레임워크 자바 : 객체 지향 언어 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크이다. 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

즉 스프링의 핵심 개념은 “객체 지향”이다

 

스프링 웹 애플리케이션 계층 구조

---- 개발자가 개발할 서비스 로직 계층
# 컨트롤러
- 클라이언트가 이용할 앤드포인트
- 클라이언트의 요청을 어떻게 처리할 지 정의
- 클라이언트의 요청을 받고 비지니스 로직을 실행하는 계층
# 서비스
- 핵심적인 비지니스 로직을 수행하는 계층
# 리포지토리
- DB에 직접적인 접근을 하는 계층 (CRUD)
- DB에 테이블과 도메인 엔티티를 매핑하는 역할을 함

그외에 스프링은 개발자가 백엔드 개발을 돕도록하는 수많은 계층이 존재

 

 

[부록] 좋은 객체 지향 프로그래밍이란

좋은 객체 지향 프로그래밍(OOP, Object-Oriented Programming)은 소프트웨어의 유연성, 재사용성, 확장성을 높이는 데 중점을 두는 프로그래밍 패러다임이다. 이를 위해 다음과 같은 원칙과 특징을 지니고 있다:

  1. 캡슐화(Encapsulation): 객체의 상태(데이터)와 행동(메소드)을 하나의 단위로 묶고, 실제 구현 내용 일부를 외부에서 볼 수 없도록 숨긴다. 이는 객체의 상세 구현을 숨기고 사용자에게는 필요한 인터페이스만을 제공하여 코드의 안정성을 높인다.
  2. 상속(Inheritance): 이미 있는 클래스의 속성과 기능을 그대로 받아 새로운 클래스를 생성하는 것이다. 코드의 재사용성을 높이고, 중복을 최소화하여 유지 보수의 편의성을 제공한다.
  3. 다형성(Polymorphism): 같은 이름의 메소드가 여러 객체 내에서 다른 방식으로 작동할 수 있게 한다. 이를 통해 코드의 유연성을 높이고, 객체 간의 상호작용을 보다 유연하게 만든다.
  4. 추상화(Abstraction): 복잡한 실제 세계를 간단한 모델로 표현하는 것이다. 필요한 정보만을 추출하여 프로그램에 반영함으로써, 복잡도를 관리하고 직관적인 코드를 작성할 수 있다.

좋은 객체 지향 프로그래밍을 실천하기 위해 다음과 같은 사항을 고려해야 한다:

SOLID [예제로 설명]

  • SRP단일 책임 원칙(Single Responsibility Principle): 한 클래스는 하나의 책임만 가져야 한다.
  • OCP개방-폐쇄 원칙(Open-Closed Principle): 소프트웨어 구성 요소는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
    • 예)
    <MemberService 클래스 내부>
    MemberRepository 
    
    MemberRepository m = new MemoryMemberRepsitory(); // 기존 코드
    MemberRepository m = new JdbcMemberRepository(); // 변경 코드
    
    MemberRepository m = new MemberRepository();
    
    # 구현 객체를 변경하려면 클라이언트 코드를 변경해야함
    # 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
    
  • LSP리스코프 치환 원칙(Liskov Substitution Principle): 하위 클래스는 언제나 상위 클래스로 대체할 수 있어야 한다.
  • ISP인터페이스 분리 원칙(Interface Segregation Principle): 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
  • DIP의존성 역전 원칙(Dependency Inversion Principle): 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
  • 위에 OCP 예제와 같이 MemberRepository 라는 역할에 의존적이어야 하지만 다향성 만으로는 구현에 의존적일 수 밖에 없다.
위에 OCP DIP 는 자바의 다향성 만으로는 해결하기 어려운 구조를 가지고 있다.

하지만 스프링 프레임워크는 OCP와 DIP를 IOC 또는 DI라 불리는 방법으로 이를 해결한다.

 

@Configuration

역할에 의존하는 법

위에 예제처럼 계층끼리 연관되어있다면

MemberService는 구현에 의존적이었다.

하지만 다음과 같이 설계한다면

계층 사이에 역할에 의존적일 수 있게 된다.

 

 

따로 AppConfig 라는 클래스가

 MemberService 에 MemberRepository 필드를 생성자를 이용해서 주입하였다.

 

AppConfig 클래스에는 MemberRepository의 구현을 주입해서 MemberService를 생성하였다

 

이로써 MemberService는 MemberRepository라는 역할에 의존적인 개발을 할 수 있게 된다.

# 그러므로 만약 MemberRepository의 구현을 변경할 때는 AppConfig 클래스만 수정하면 된다.

 

 

용어설명

제어의 역전 IoC(Inversion of Control)

  • 위 예제처럼 config 파일로 인해 의존관계를 주입한다.
  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다

의존관계 주입DI(Dependency Injection)

  • MemberServiceImpl은 MemberRepository 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
  • 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.

정적인 클래스 의존 관계와 동적인 객체 의존 관계

둘 의존 관계의 차이점은 실제 코드 상의 의존 관계는 정적인 클래스 의존 관계이다.

동적인 객체 의존 관계는 런타임 즉 실행시에 결정되는 객체의 의존관계를 의미한다.

위 예제에서는 인터페이스가 정적인 클래스 의존

구현체가 동적 의존 관계임을 알 수 있다.

 

 

@Configuration

  • 그럼 프로그램 개발 시 일일히 Config 클래스를 생성해서 실행햐 할까?
  • @Configuration 을 사용하면 다음과 같이 의존관계를 프로그램 시작 시 자동으로 DI 해준다.
@Configuration
public class AppConfig {
 @Bean
 public MemberService memberService() {
 return new MemberServiceImpl(memberRepository());
 }

 @Bean
 public MemberRepository memberRepository() {
 return new MemoryMemberRepository();
 }

}
@Configuration 을 사용한다면
스프링 컨테이너
에서 Bean에 해당하는 메서드를 실행하여 객체를 생성하고 관리한다.

 

그럼 이런 @Configuration 을 사용하면 자동으로 Bean에 해당하는 객체를 누가 DI 해주는 것일까

어떻게 @Configuration 이 이런걸 해줄까

다음 코드를 실행한 결과이다.

AppConfig 클래스를 출력해보았다.

그런데 이상한 문자가 추가되어있다.

이는 스프링이 @Configuration 어노테이션이 있는 클래스를 상속하는 저 이상한 이름의 클래스를
만든 것이다.

그래서 자바의 상속을 이용해 Bean 을 생성하고 주입해주는 것이다.

위 같이 클래스를 상속으로 변경하여 조작하는 패턴을 프록시 패턴이라 한다.
(위 같은 방법이 스프링을 쓰다보면 생각보다 많다.)

스프링 컨테이너

  • ApplicationContext 를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
  • 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다.
  • 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다

💡 스프링 컨테이너(혹은 빈 컨테이너라고도 부름) 는 @Configuration 어노테이션이 붙은 파일을 참고하여 bean을 저장하는데

이때 생성자가 필요한 bean의 경우는 생성을 미루고 다른 bean을 등록하여 의존관계에 해당하는 bean을 생성한다

빈의 이름은 클래스명의 첫글자를 소문자로 등록하는 것이 관례이다.

(물론 변경할 수 있다)

 

[참고]

BeanFactory 는 스프링 빈을 관리하고 빈을 검색 생성을 담당하는 로직이 담겨있다.

이러한 기능을 제공해주는 클래스는 ApplicationContext클래스중 config 클래스는 
AnnotationConfigApplicationContext가 Config를 읽고 빈을 생성한다.

AnnotationConfigApplicationContext는 @Configuration 어노테이션 config 파일을 실행한다고
생각하면된다.
<더 자세한 기능이 궁금하면 AnnotaionConfigApplicationContext 공식문서를 확인하는 것을 추천>

스프링 Bean과 싱글톤

  • 스프링이 자동으로 의존관계를 DI 해주는건 좋은거 같지만 왜 생성하고 관리까지 해줄까?
    • 싱글톤 때문

 

#스프링 컨테이너가 관리하지 않는 객체는 클라이언트의 요청마다 위 사진과 같이 

MemberService를 생성한다.

하지만 이는 다음과 같은 문제점을 만들기 때문에 스프링은 싱글톤을 지원한다.
  • 이전까지 설계한 AppConfig 는 클라이언트의 service 요청마다 memberService 객체를 생성했다.
  • 이런 설계 패턴은 작은 트래픽에서는 상관없지만 많은 트래픽을 감수하는 웹 애플리케이션에서는 비효율적이다

→ 이에 대한 해결 방법은 싱글톤 패턴이다.

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴을 의미
    • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    public class Singleton {
        // 유일한 인스턴스를 저장할 변수
        private static Singleton instance;
    
        // 외부에서 인스턴스를 생성하지 못하도록 생성자를 private으로 선언
        private Singleton() {}
    
        // 인스턴스에 접근할 수 있는 public 메소드
        public static Singleton getInstance() {
            // 인스턴스가 생성되어 있지 않으면 새로 생성
            if (instance == null) {
                instance = new Singleton();
            }
            // 생성된 인스턴스 반환
            return instance;
        }
    }
    
    
  • 위 예제는 싱글톤을 구현하는 대표적인 예시이다.
  • 객체를 하나만 생성해야기에 정적변수의 자기 자신의 인스턴스를 생성한다.
  • 다른 곳에서 SingletonService의 인스턴스를 생성하는 것을 막기위해 private으로 생성자를 선언
  • singleton 인스턴스의 접근을 위해 getter 생성해준다.

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
  • 지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
    • 싱글톤 패턴을 위한 지저분한 코드 X
    • DIP , OCP , private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.

 

컴포넌트 스캔이란

  • 설정 파일 (Config) 파일을 통해 bean 을 수동으로 설정하고 의존관계를 수동으로 대입하는 것은 DIP와 OCP를 잘 지키는 방법이지만, 등록해야 할 스프링 빈이 수십 , 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고 , 누락하는 문제도 발생한다.
  • 이를 위해 스프링은 설정 정보가 없어도 자동으로 스플이 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 또 의존 관계를 자동으로 주입하는 @Autowired라는 기능도 제공한다.

 

 

  • 위와 같이 ComponentScan 이라는 어노테이션을 사용하면 특정 범위에 있는 @Component 어노테이션을 가지고 있는 클래스를 bean 등록을 한다.

자동 주입

 

 

  • 그리고 Config 파일 처럼 bean을 지정하고 의존 관계를 설정하는 코드가 없기 때문에 다른 방식의 DI 가 필요하다.
  • 이를 위해 스프링은 AutoWired라는 어노테이션 기능을 제공한다.

동작 방식

    • 이 때 bean 의 이름은 첫글자를 소문자로 하는 것이 기본 규칙이다.
    • 빈 이름을 직접 지정하는 것도 가능하다컴포넌트 어노테이션이 붙은 클래스를 객체를 생성하고 bean으로 등록한다.

 

    • getBean(MemberRepository.class) 처럼 타입이 같은 빈을 찾아서 주입한다.생성자에 Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입

 

스프링 공부를 하다보면 Component 어노테이션이 아닌 Controller Service Repository 에 해당하는 어노테이션을 본적 있을 것이다.

이는 다른 어노테이션이 아닌 Component에 추가적으로 계층에 맞는 기능을 위해 어노테이션을 합친것이다.

 

중복 등록과 충돌

  • bean 등록 시 이름이 같은 bean 이 등록 될 시 에러가 발생한다.
  • 자동 빈 끼리 충돌이나면 에러가 나고
  • 수동 과 자동은 원래 수동이 우선권을 가져 자동을 오버라이딩 했지만 최근 스프링 부트는 에러를 발생한다.

 충돌을 우려할 상황이 나오면 Bean이름을 다르게 하던가 Bean 탐색에 우선순위 고유 id 를 사용할 수 있다 → 이는 필요한 순간에 찾아보면 좋을듯 하다

 

다양한 의존관계 주입 방법[다음 챕터]

  1. 생성자 주입
  2. 수정자 주입
  3. 필드 주입
  4. 일반 메서드 주입