-
[Spring Boot with Kotlin] Spring : MVC, IoC, DI, AOPSpringBoot 2024. 1. 3. 23:11
Spring의 핵심 개념인 MVC 모델, IoC, DI, AOP에 대해 알아보자.
____________________________________________________________________________________
1. Spring
Spring은 RESTful한 웹 애플리케이션을 구축하기 위해 포괄적인 지원을 제공하는 Java/Kotlin 기반의 오픈 소스 *프레임워크로, 스프링 MVC 모듈을 통해 REST 원칙을 쉽게 구현할 수 있어, 구현 프로세스를 간소화하고, 개발자가 애플리케이션의 로직 작성에만 집중할 수 있도록 하여, 확장과 유지보수가 용이한 웹 애플리케이션을 설계할 수 있도록 지원해준다. Spring은 2004년 3월에 출시된 이래로, 20년의 세월 동안 JAVA 엔터프라이즈 애플리케이션 개발의 선두 자리를 잃지 않고 있다. 최근 많은 서버들이 *마이크로서비스 아키텍처로 만들어지고 있는데, Spring도 이를 지원하기 위해 개발/업데이트를 진행하는 것을 보면, Spring은 시대에 변화에 뒤쳐지지 않게 진화하는 유연한 프레임워크라는 것을 알 수 있다.
※ 프레임워크 (Framework): 애플리케이션을 개발하기 위한 규약과 기능을 제공하는 틀
※ 아키텍처의 종류: 모놀리식 (Monolithic) vs 마이크로서비스 (Microservice)
(추후 업데이트 예정)____________________________________________________________________________________
2. Spring의 특징
1) MVC (Model-View-Controller)
애플리케이션을 모델(데이터), 뷰(프레젠테이션), 컨트롤러(흐름)의 3가지 상호 연결된 구성 요소로 구분하는 MVC 패턴은 웹 애플리케이션에서 가장 널리 사용되는 아키텍처 패턴이다. Spring 내에 존재하는 MVC 모듈을 통해 Spring Framework에서 MVC 패턴을 지원하며, 개발자는 이를 사용하여 유연하게 RESTful한 웹 애플리케이션을 설계할 수 있다.
사진 출처: https://shreysharma.com/mvc-architecture/ MVC 패턴에서 M(모델)은 서버 상에서 작동하는 여러 데이터와 비즈니스 로직을 나타내고, V(View)는 프레젠테이션 계층, 즉 클라이언트에게 보여지는 화면을 나타내며, C(Controller)는 클라이언트와 서버 간의 리소스를 통한 상호 작용과 데이터 흐름을 조작한다. Spring Framework는 다양한 어노테이션과 추상화를 제공하여 클라이언트와 서버 간의 요청/응답 과정을 제어하고, 서버가 내부 로직을 처리할 수 있게끔 지원한다.
_
2) IoC (Inversion of Control)
Spring에서는 일반적으로 개발자가 객체를 (Java 기준) new 키워드를 통해 직접 생성하는 경우는 거의 없다. 개발자가 객체의 *의존성을 자체적으로 관리하는 것이 아니라, 객체의 생성부터 시작해서 객체의 모든 생명 주기를 Spring Container가 책임지기 때문에, 즉 객체 관리의 모든 책임을 Spring에 위임함으로써, 개발자는 오로지 내부 로직 구현에만 집중할 수 있어 코드 양을 획기적으로 줄일 수 있고, 손쉽게 클래스를 테스트하고 유지/관리할 수 있다. 이렇듯 일반적인 프로그래밍과 달리, 개발자가 아닌 Framework가 객체를 제어하는 권한을 가져, 제어의 반전된 흐름을 가진 설계 원칙을 바로 IoC라고 한다.
※ 의존성(Dependency): 특정 객체가 다른 객체를 참조하는 것을 의미한다.
_
3) DI (Dependency Injection)
Spring Container는 객체의 생명 주기를 대신 관리해주는데, 그렇다면 개발자는 객체를 어떻게 사용할까? 개발자는 객체를 Spring Container로부터 주입을 받아 사용한다. Spring은 개발자가 특정 어노테이션을 통해 지정한 객체를 Spring Container에 등록해주고, 생성부터 삭제까지 모든 생명 주기를 관리하기 때문에, 개발자는 객체 생성 과정에 일절 관여하지 않아 객체 사이의 의존도를 낮출 수 있다. 즉, 객체들이 약한 결합으로 관계를 맺는다. 개발자가 작성한 코드에 변경 사항이 생겨도, 그 영향을 받는 다른 객체의 수가 최소화되기 때문에 코드를 확장하기에도 용이하다. 또한, 코드를 작성하다보면 객체 A가 객체 B를 참조하고, 객체 B가 또 다시 객체 A를 참조하는, '순환 참조'로 인한 에러가 발생하는 경우가 종종 있는데, 객체를 외부로부터 주입받게 되면 순환 참조를 방지할 수 있어, 코드의 안정성이 증가하고 개발 시간을 절약할 수 있다.
엥? 근데 IoC에서 했던 설명이랑 내용이 거의 비슷하다? 맞다. IoC를 구현한 가장 대표적인 예가 DI다.
Spring은 Setter Injection, Field Injection, Constructor Injection 등 다양한 매커니즘을 통해 DI를 지원하는데, 테스트 코드 작성이 쉽고, 객체의 불변성을 보장하는 Constructor Injection을 사용하는 것이 권장되고 있다. 의존성 주입의 여러 방법에 대해서는 추후에 추가적인 게시글을 통해 구체적으로 알아보도록 하자.
※ 참고로, Spring Container는 DI Container, 혹은 IoC Container라고도 불린다.
_
4) 테스트의 용이성
테스트는 애플리케이션 개발의 필수 요소 중 하나이며, 특히 RESTful 웹 애플리케이션에서는, REST *엔드포인트의 동작과 정확성을 검증하기 위해 테스트는 배포 이전에 반드시 거쳐야 하는 과정이다. 그러나, Spring이 개발되기 전(00년대 초) 과거 Java 웹 애플리케이션에서는 코드 테스트를 하는 방법이 굉장히 어려웠다. 단순한 코드를 테스트하기 위해서 애플리케이션을 배포를 해야하거나, 테스트가 데이터베이스에 영향을 주는 등, 외부 의존도가 높은 코드에 대해 테스트하기가 굉장히 어려웠는데, Spring은 이런 문제점을 IoC, DI의 개념을 도입하여 해결하였다. 객체들이 약한 결합으로 관계를 가가지기 때문에, 구성 요소를 분리하여 테스트할 수 있다, 예를 들어, 객체 A가 객체 B에 의존하는 경우, 개발자는 객체 A를 객체 B와 분리하여, 객체 B의 영향을 받지 않은 채 오로지 객체 A만 테스트할 수 있는 것이다. DI를 통해 객체 간의 결합도를 낮추었기 때문에 가능한 얘기다. 이 뿐만 아니라, Spring Framework는 여러 모듈과 어노테이션을 제공함으로써 애플리케이션의 품질을 보장할 수 있는 강력한 테스트 기능들을 제공한다.
※ 엔드포인트: API가 서버에 접근할 수 있도록 명시해주는 리소스의 위치
(Ex. .../members/100, .../pages/comments)_
5) AOP (Aspect-Oriented Programming)
AOP는 애플리케이션에서 여러 클래스와 모듈들을 횡단하는 관심사들을 핵심 비즈니스 로직으로부터 분리하기 위해 사용되는 패러다임으로, 관점(Aspect)이라는 단위를 사용하여 구현된다. '관점'은 애플리케이션 내에서 핵심 로직은 아니지만, Controller, Service, Repository 등 애플리케이션 내에서 (쉽게 예측할 수 없는) 모든 지점을 가로질러야 하는 공통의 부가적인 로직들을 언제/어떻게 적용할 것인지 판단하는 개념이다.
'부가'적인 로직이라고 해서 절대로 '애플리케이션 내에서 중요하지 않은' 기능이라는 뜻이 아니다!! 단순히 핵심 비즈니스 로직이 아닌 나머지 로직을 의미한다. 여기에는 로깅, 보안, 트랜잭션 등이 포함된다.
예를 들어, 할 일 목록들을 만들어 관리하는 To-Do 애플리케이션이 있다고 가정하자. To-Do 애플리케이션의 핵심이 될 만한 기능에는 아무래도 할 일에 대한 CRUD 작업, 이미 수행한 할 일에 대한 완료 처리 등이 있을텐데, 이 모든 핵심 기능들은 이미 구현 및 배포가 된 상황이다. 이 때, 클라이언트로부터 최근 To-Do 애플리케이션의 작업 처리 속도가 느려졌다는 피드백을 받고, 애플리케이션 내 모든 메서드의 호출 시간을 측정해서, 메모리를 지나치게 많이 점유하는 메서드의 알고리즘을 수정하고자 한다. 이 때, '메서드의 호출 시간을 측정하는 기능'은 절대로 To-Do 애플리케이션의 핵심 기능이 아니다. 다르게 말해서, '메서드의 호출 시간을 측정하는 기능'은 모든 애플리케이션 내에 '공통'으로 적용되어야 하는 기능이지, 특정 메서드에 국소적으로만 구현되는 기능이 아니라는 것이다. 그렇다면 모든 메서드마다 호출 시간 측정 로직을 작성해야 할까? 절대 아니다. 해당 로직을 구현한 코드 수가 8줄, 애플리케이션 내 메서드의 수가 10개라고 가정하면 8x10(=80)줄의 코드를 낭비하게 되는데...? 따라서, 아무리 모든 메서드에 적용되는 기능이라고 하더라도, 핵심 로직과 다른 위치에 별도로 관리를 해야한다.
위 예시를 구현한 패키지 구조다. 핵심 기능들은 domian 패키지 안에 두고, 예외 처리, 처리 속도 측정, 기타 설정 파일 등 애플리케이션 내 전반적으로 적용되는 기능들은 별도의 패키지로 구성한다. 이런 식으로 관심사의 적용 범위에 따라 코드를 분리(핵심 로직 vs 공통 로직)하는 매커니즘이 바로 AOP다.
하나의 메서드에 핵심 로직과 호출 시간을 측정하는 로직이 섞여있는 경우, 호출 시간 측정 로직에 일부 수정 사항이 생기면, 모든 메서드를 찾아가서 해당 로직을 전부 일일이 수정해야 하기 때문에 유지보수에 불편함이 발생한다. 당연한 얘기지만, 코드의 수가 늘어나 가독성이 떨어지는 문제 역시 발생한다. 이를 해결하기 위해, 개발자가 '시간 측정 로직'을 하나의 클래스에 모아두어 Spring Container에 등록만 해두면, Spring은 알아서 원하는 지점에 공통 관심 사항을 적용시켜준다. 이렇게 핵심 관심 사항(core concern)으로부터 공통 관심 사항(cross-cutting concern)을 분리함으로써, 코드 가독성과 재사용성을 높이고, 애플리케이션을 유지/관리를 더 용이하게 해준다.
AOP가 작동하는 세부적인 방식은 추후에 추가적인 게시글을 통해 알아보도록 하자.
____________________________________________________________________________________