1. Framework
1-1. Framework란?
“소프트웨어의 구체적인 부분에 해당하는 설계와 구현을 재사용이 가능하게끔 일련의 협업화된 형태로 클래스들을 제공하는 것” 이라고 정의하고 있지만, 이 문장만으로 소프트웨어 관점에서 Framework의 의미를 이해하기는 쉽지 않다.
하나의 애플리케이션을 건물이라고 가정한다면, Frame은 건물의 구조라고 생각하면 된다.
즉, 소프트웨어 관점에서의 Framework는 어떤 애플리케이션을 만들기 위한 틀 혹은 구조를 제공하는 것이다.
그렇다면 Framework를 사용했을 때 장점과 단점은 무엇일까?
장점
- 효율적으로 코드를 작성할 수 있다.
서로 다른 애플리케이션 간의 통신이나, 데이터를 데이터 저장소에 저장하는 등의 다양한 기능들은 Framework에서 라이브러리 형태로 제공함으로써 애플리케이션의 핵심 로직을 개발하는 것에 집중할 수 있도록 해준다. - 정해진 규약이 있어 애플리케이션을 효율적으로 관리할 수 있다.
사용하는 Framework의 규약에 맞게 코드를 작성하기 때문에, 유지 보수가 필요한 경우 더 빠르고 쉽게 문제점을 파악해 수정할 수 있다. 또한 내가 작업한 코드를 다른 사람이 수정하거나 다른 사람의 코드를 내가 수정할 경우에도 빠르게 코드를 파악할 수 있다.
단점
- 사용하고자 하는 Framework에 대한 학습이 필요하다.
- 자유롭고 유연한 개발이 어렵다.
1-2. Framework와 Library의 차이점
소프트웨어 관점에서의 Library는 애플리케이션을 개발하는 데 사용되는 일련의 데이터 및 프로그래밍 코드를 의미한다. 즉, 애플리케이션을 개발할 때 필요한 기능을 미리 구현해놓은 집합체라고 할 수 있다.
하나의 자동차를 예로 살펴보자.
Framework는 자동차의 뼈대, 다시 말하면 Frame을 의미하며 Library는 자동차의 부품이라고 할 수 있다.
자동차의 Frame을 교체하는 건 거의 불가능하지만 바퀴나 와이퍼 같은 부품은 교체가 쉽다.
이 말은 소프트웨어 관점에서도 Framework를 교체하는 일은 어렵지만, Library는 쉽게 교체가 가능하다고 해석이 가능하다.
정리하자면 Framework는 Library와 애플리케이션에 대한 제어권의 차이가 있다는 것이다.
@SpringBootApplication
@RestController
@RequestMapping(path = "/v1/message")
public class SampleApplication {
@GetMapping
public String getMessage() { // (2)
String message = "hello world";
return StringUtils.upperCase(message); // (1)
}
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
위 코드에서 명확하게 Library를 사용하는 부분은 (1)StringUtils.upperCase(message) 이다.
여기서 StringUtils 클래스는 Apache Commons Lang3 라이브러리의 유틸리티 클래스 중 하나이며 StringUtils클래스의 upperCase() 메서드를 사용하고 있음을 알 수 있다.
이렇게 개발자가 필요한 기능을 해당 라이브러리를 호출해서 사용하는 것이 Library다.
이번에는 Framework를 살펴보자.
(2)getMessage() 메서드 내부의 코드처럼 개발자가 메서드 내에 코드를 작성해두면 Spring Framework에서 개발자가 작성한 코드를 사용해 애플리케이션의 흐름을 만들어낸다.
즉, 애플리케이션의 흐름의 주도권이 개발자가 아닌 Framework에 있는 것이다.
1-3. Spring Framework란?
웹 애플리케이션 개발을 위한 Framework는 Spring 뿐만 아니라, Django, Express, Flask, Lalavel 등이 있다.
그렇다면 Spring Framework를 사용하는 이유는 무엇일까?
- POJO(Plan Old Java Object)기반의 구성
- DI(Dependency Injection) 지원
- AOP(Aspect Oriented Programming, 관점 지향 프로그래밍) 지원
- Java 언어를 사용함으로써 얻는 장점
2. Spring Framework
위 그림은 Spring 삼각형이라고 불리는데, 이 삼각형 하나로 Spring의 핵심 개념들을 모두 표현하고 있다고 해도 과언이 아니다.
지금부터 핵심 개념들을 하나씩 알아보자.
2-1. POJO
POJO란 Plain Old Java Object의 약자로 오래된 방식의 순수한 자바객체라고 할 수 있다.
POJO 프로그래밍으로 작성한 코드라고 하기 위해서는 두 가지의 기본적인 규칙은 지켜야 한다.
- Java나 Java의 스펙(사양)에 정의된 것 이외에는 다른 기술이나 규약에 얽매이지 않아야 한다.
public class User {
private String userName;
private String id;
private String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
자바에서 제공하는 기능만 사용하여 getter, setter만 가지고 있는 코드이다. Java 언어 이외에 특정한 기술에 종속되어 있지 않은 순수한 객체이기 때문에 POJO라고 부를 수 있다.
public class MessageForm extends ActionForm{ // (1)
String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
public class MessageAction extends Action{ // (2)
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
MessageForm messageForm = (MessageForm) form;
messageForm .setMessage("Hello World");
return mapping.findForward("success");
}
}
ActionForm 클래스와 Action 클래스는 Struts라는 웹 프레임워크에서 지원하는 클래스다. 이렇게 특정 기술을 상속해서 코드를 작성하게 되면 나중에 애플리케이션의 요구사항이 변경돼서 다른 기술로 변경하려면 Struts의 클래스를 명시적으로 사용했던 부분을 전부 다 일일이 제거하거나 수정해야 한다. 그리고 Java는 다중 상속을 지원하지 않기 때문에 객체지향 설계 기법을 적용하기 어려워진다.
- 특정 환경에 종속적이지 않아야 한다.
순수 Java로 작성한 애플리케이션 코드 내에서 Tomcat이 지원하는 API를 직접 가져다가 사용한다고 가정해보자. 만약 시스템 요구사항이 변경돼서 Tomcat 말고 Zetty라는 다른 Servlet Container를 사용하게 된다면 어떨까? 최악의 경우 애플리케이션을 전부 뜯어 고쳐야 될지도 모른다.
POJO 프로그래밍이 필요한 이유를 정리해보자.
- 특정 환경이나 기술에 종속적이지 않으면 재사용과 확장이 가능한 유연한 코드를 작성할 수 있다.
- 저수준 레벨의 기술과 환경에 종속적인 코드를 애플리케이션 코드에서 제거함으로써 코드가 간결해진다.
- 코드가 깔끔해지기 때문에 디버깅이 상대적으로 쉽다.
- 특정 기술이나 환경에 종속적이지 않아서 테스트 역시 단순하다.
- 객체지향적인 설계를 제한 없이 적용할 수 있다.
아래는 POJO 프로그래밍을 작성하기 위해 Spring에서 지원하는 세 가지 기술이다.
2-2. IoC
IoC(Inversion of Control)란 애플리케이션 흐름의 주도권을 Spring이 갖는다는 것이다.
public class Example2_10 {
public static void main(String[] args) {
System.out.println("Hello IoC!");
}
}
일반적으로 이 코드와 같은 Java 콘솔 애플리케이션을 실행하려면 main() 메서드가 있어야 한다.
main() 메서드가 호출된 후 System 클래스를 통해 println()을 호출한다.
이렇게 개발자가 작성한 코드를 순차적으로 실행하는 게 애플리케이션의 일반적인 제어 흐름이다.
이번에는 Java 콘솔 애플리케이션이 아니라 웹 상에서 돌아가는 Java 웹 애플리케이션의 경우를 생각해보자.

웹에서 동작하는 애플리케이션의 경우 클라이언트가 외부에서 접속해서 사용하는 서비스이기 때문에 main() 메서드가 종료되지 않아야 한다.
그런데 서블릿 컨테이너에서는 서블릿 사양(Specification)에 맞게 작성된 서블릿 클래스만 존재하지 별도의 main() 메서드가 존재하지 않는다.
main() 메서드가 없는데 어떻게 애플리케이션이 실행되는 걸까?
서블릿 컨테이너의 경우, 클라이언트의 요청이 들어올 때마다 서블릿 컨테이너 내의 컨테이너 로직(service() 메서드)이 서블릿을 직접 실행시켜 준다.
서블릿 컨테이너가 서블릿을 제어하고 있기 때문에 애플리케이션의 주도권은 서블릿 컨테이너에 있다.
바로 서블릿과 웹 애플리케이션 간에 IoC(제어의 역전)의 개념이 적용되어 있는 것이다.
Spring에서는 이 IoC의 개념이 DI(Dependency Injection)를 통해 적용되어 있다.
2-3. DI
DI(Dependency Injection)는 IoC 개념을 조금 구체화 시킨 것이라고 볼 수 있다.
DI를 의미 그대로 해석하면 의존성 주입이 되는데 이 “의존성 주입” 이라는 용어에 대해 육하원칙에 따라 단계적으로 살펴보자.
- 의존성 주입은 무엇일까?
A 클래스가 B 클래스의 기능을 사용할 때 “A 클래스는 B 클래스에 의존한다”고 표현한다.
public class MenuController {
public static void main(String[] args) {
MenuService menuService = new MenuService();
List<Menu> menuList = menuService.getMenuList();
}
}
public class MenuService {
public List<Menu> getMenuList() {
return null;
}
}
MenuController 클래스는 클라이언트의 요청을 받는 엔드포인트(Endpoint) 역할을 하고, MenuService 클래스는 MenuController 클래스가 전달 받은 클라이언트의 요청을 처리하는 역할을 하고 있다.
MenuController 클래스는 MenuService 클래스의 객체를 생성한 후, 이 객체로 getMenuList() 메서드를 호출하고 있다.
이처럼 사용하고자 하는 클래스의 객체를 생성해서 참조하게 되면 의존 관계가 성립한다.
아직까지는 의존 관계는 성립되었지만 의존성 주입은 이루어지지 않았다.
public class CafeClient {
public static void main(String[] args) {
MenuService menuService = new MenuService();
MenuController controller = new MenuController(menuService);
List<Menu> menuList = controller.getMenus();
}
}
public class MenuController {
private MenuService menuService;
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public List<Menu> getMenus() {
return menuService.getMenuList();
}
}
public class MenuService {
public List<Menu> getMenuList() {
returl null;
}
}
MenuController 클래스에서 MenuService의 객체를 직접 생성하지 않고 MenuController 생성자로 MenuService의 객체를 전달 받고 있다.
이처럼 생성자를 통해 어떤 클래스의 객체를 전달 받는 것을 “의존성 주입”이라고 한다.
생성자의 파라미터로 객체를 전달하는 것을 “외부에서 객체를 주입한다”고 표현한다.
여기에서는 CafeClient 클래스가 MenuController의 생성자 파라미터로 menuService를 전달하고 있기 때문에 객체를 주입해주는 “외부”가 되는 것이다.
만약 의존성 주입이 정확하게 이해가 되지 않는다면 딱 하나만 기억하자.
💡 클래스의 생성자로 객체를 전달 받는 코드가 있다면 의존성 주입이 이루어지는 것이다.
2. 의존성 주입이 왜 필요할까?
Java에서 생성자를 통해 객체를 전달하는 일은 아주 흔한 일이다. 즉, 의존성 주입이 필요한 건 어찌 보면 당연하다.
그런데 의존성 주입을 사용할 때 항상 염두에 두어야 하는 부분이 한 가지 있다.
현재의 클래스 내부에서 외부 클래스의 객체를 생성하기 위한 new 키워드를 쓸지 말지 여부를 결정하는 것이다.
애플리케이션 코드 내부에서 직접적으로 new 키워드를 사용할 경우 객체지향 설계의 관점에서 중요한 문제가 발생할 수 있다.
예를 들어 참조할 클래스가 바뀌는 경우 이 클래스를 사용하는 모든 클래스를 수정해야 한다.
이렇게 new 키워드를 사용해서 의존 객체를 생성할 때 클래스들 간에 강하게 결합(Tight Coupling) 되어 있다고 한다.
결론적으로 의존성 주입을 하더라도 클래스들 간의 느슨한 결합(Loose Coupling)이 필요하다.
3. 느슨한 의존성 주입은 어떻게 해야 할까?
대표적인 방법은 인터페이스를 사용하는 것이다.

public class CafeClient {
public static void main(String[] args) {
MenuService menuService = new MenuServiceStub();
MenuController controller = new MenuController(menuService);
List<Menu> menuList = controller.getMenus();
}
}
public class MenuController {
private MenuService menuService;
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public List<Menu> getMenus() {
return menuService.getMenuList();
}
}
public interface MenuService {
List<Menu> getMenuList();
}
public class MenuServiceStub implements MenuService {
public List<Menu> getMenuList() {
return List.of(
new Menu(1, "아메리카노", 2500),
new Menu(2, "카라멜 마끼아또", 4500),
new Menu(3, "바닐라 라떼", 4500)
);
}
}
3-4번 줄에서 new 로 MenuServiceStub 클래스의 객체를 생성해서 MenuService 인터페이스에 할당하고 있다.
이처럼 인터페이스 타입의 변수에 그 인터페이스의 구현 객체를 할당할 수 있는데 이를 업캐스팅(Upcasting)이라고 한다.
하지만 클래스들의 관계를 느슨하게 만들기 위해서는 new 키워드를 사용하지 않아야 되는데, 여전히 new를 사용하고 있다.
이 new 키워드는 어떻게 하면 제거하고 의존 관계를 느슨하게 만들 수 있을까?
바로 Spring이 대신 해준다.
4. Spring 기반 애플리케이션에서는 의존성 주입을 누가 해줄까?
public class CafeClient {
public static void main(String[] args) {
GenericApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
MenuController controller = context.getBean(MenuController.class);
List<Menu> menuList = controller.getMenus();
}
}
public class MenuController {
private MenuService menuService;
@Autowired
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public List<Menu> getMenus() {
return menuService.getMenuList();
}
}
@Configuration
@ComponentScan(basePackageClasses = cafeClient.class)
public class Config {
@Bean
public MenuService getMenuService() {
return new MenuServiceStub();
}
@Bean
public MenuController getMenuController(MenuService menuService) {
return new MenuController(menuService);
}
}
3-5번 줄 Spring에서 지원하는 API 코드가 new 키워드를 대신해서 객체를 생성해주고 있다.
앞서 POJO 프로그래밍 규칙 중 “다른 기술이나 규약에 얽매이지 않아야 한다”가 있었는데 여기에서 Spring Framework에 해당하는 코드가 직접적으로 작성되어 있다.
의존성 주입의 예를 보여주기 위해 사용한 코드이기 때문이며 실제 Spring 기반의 웹 애플리케이션에는 나오지 않는다.
Config 클래스에 정의해둔 MenuController 객체를 Spring의 도움을 받아 CafeClient 클래스에 제공하고 있다.
Config 클래스 안에서 new 키워드로 객체를 생성하고 있지만 Config는 Spring Framework의 영역에 해당하는 것이고, 이 Config 클래스가 실제 애플리케이션의 핵심 로직에 관여하지 않는다.
2-4. AOP
AOP(Aspect Oriented Programming)은 관심 지향 프로그래밍이다.
애플리케이션을 개발하다 보면 애플리케이션 전반에 걸쳐 공통적으로 사용되는 기능들이 있는데, 이러한 공통 기능들에 대한 관심사를 바로 공통 관심 사항(Cross-cutting concern)이라고 한다.
그리고 흔히 말하는 비즈니스 로직, 즉 애플리케이션의 주목적을 달성하기 위한 핵심 로직에 대한 관심사를 핵심 관심 사항(Core concern)이라고 한다.
커피 전문 애플리케이션을 예로 들면, 사장이 커피 종류를 등록하는 기능과 고객이 커피를 주문하는 기능은 핵심 관심 사항에 해당된다.
애플리케이션 보안에 대한 부분은 애플리케이션 전반에 공통적으로 적용되는 기능이기 때문에 공통 관심 사항에 해당된다.
AOP는 애플리케이션의 핵심 업무 로직에서 로깅이나 보안, 트랜잭션 같은 공통 기능 로직들을 분리하는 것이다.
public class Example2_11 {
private Connection connection;
public void registerMember(Member member, Point point) throws SQLException {
connection.setAutoCommit(false); // (1)
try {
saveMember(member); // (2)
savePoint(point); // (2)
connection.commit(); // (3)
} catch (SQLException e) {
connection.rollback(); // (4)
}
}
private void saveMember(Member member) throws SQLException {
PreparedStatement psMember =
connection.prepareStatement("INSERT INTO member (email, password) VALUES (?, ?)");
psMember.setString(1, member.getEmail());
psMember.setString(2, member.getPassword());
psMember.executeUpdate();
}
private void savePoint(Point point) throws SQLException {
PreparedStatement psPoint =
connection.prepareStatement("INSERT INTO point (email, point) VALUES (?, ?)");
psPoint.setString(1, point.getEmail());
psPoint.setInt(2, point.getPoint());
psPoint.executeUpdate();
}
}
registerMember() 메서드 내에서 실제로 비즈니스 로직을 수행하는 코드는 (2)saveMember(), savePoint()이다.
(1), (3), (4)의 기능들은 (2)작업을 트랜잭션으로 묶어서 처리하기 위한 기능들이다.
문제는 이렇게 트랜잭션 처리를 하는 코드들이 애플리케이션의 다른 기능에도 중복되어 나타날 것이라는 거다.
이런 경우 중복된 코드를 공통화해서 재사용이 가능하도록 만들어야 한다.
Spring에서는 이미 이런 트랜잭션 처리 기능을 AOP를 통해 공통화 해두었다.
@Component
@Transactional // (1)
public class Example2_12 {
private Connection connection;
public void registerMember(Member member, Point point) throws SQLException {
saveMember(member);
savePoint(point);
}
private void saveMember(Member member) throws SQLException {
// Spring JDBC를 이용한 회원 정보 저장
}
private void savePoint(Point point) throws SQLException {
// Spring JDBC를 이용한 포인트 정보 저장
}
}
@Transactional 애노테이션 하나만 붙이면 이 애노테이션 정보를 활용해서 트랜잭션을 적용한다.
2-5. PSA
PSA(Portable Service Abstraction)란 추상화의 개념을 애플리케이션에서 사용하는 서비스에 적용하는 기법이다.

이 그림은 Java 콘솔 애플리케이션에서 클라이언트가 데이터베이스에 연결하기 위해 JdbcConnector를 사용하기 위한 예시다.
JdbcConnector가 애플리케이션에서 이용하는 하나의 서비스가 되는 것이다.
DbClient는 JdbcConnector 인터페이스를 통해 간접적으로 연결되어(느슨한 결합) Connection 객체를 얻는 것을 볼 수 있다.
그런데 DbClient에서 어떤 JdbcConnector 구현체를 사용하더라도 Connection을 얻는 방식은 getConnection() 메서드를 사용하는 것으로 동일하다. 즉, 일관된 방식으로 해당 서비스의 기능을 사용할 수 있다.
이처럼 애플리케이션에서 특정 서비스를 이용할 때, 접근하는 방식 자체를 일관되게 유지하면서 기술 자체를 유연하게 사용할 수 있도록 서비스의 기능을 구성하는 것을 PSA라고 한다.
PSA가 필요한 주된 이유는 애플리케이션에서 사용하는 기술이 변경되더라도 최소한의 변경만으로 변경된 요구 사항을 반영하기 위함이다.
'CodeStates_BE_44 > TIL' 카테고리의 다른 글
[Spring] Spring Framework 핵심 개념_DI (0) | 2023.04.05 |
---|---|
[DB] 데이터베이스 설계 (0) | 2023.03.29 |
[DB] SQL, NoSQL (0) | 2023.03.28 |
[네트워크] HTTP (0) | 2023.03.27 |
[네트워크] 웹 애플리케이션 작동 원리 (0) | 2023.03.24 |
1. Framework
1-1. Framework란?
“소프트웨어의 구체적인 부분에 해당하는 설계와 구현을 재사용이 가능하게끔 일련의 협업화된 형태로 클래스들을 제공하는 것” 이라고 정의하고 있지만, 이 문장만으로 소프트웨어 관점에서 Framework의 의미를 이해하기는 쉽지 않다.
하나의 애플리케이션을 건물이라고 가정한다면, Frame은 건물의 구조라고 생각하면 된다.
즉, 소프트웨어 관점에서의 Framework는 어떤 애플리케이션을 만들기 위한 틀 혹은 구조를 제공하는 것이다.
그렇다면 Framework를 사용했을 때 장점과 단점은 무엇일까?
장점
- 효율적으로 코드를 작성할 수 있다.
서로 다른 애플리케이션 간의 통신이나, 데이터를 데이터 저장소에 저장하는 등의 다양한 기능들은 Framework에서 라이브러리 형태로 제공함으로써 애플리케이션의 핵심 로직을 개발하는 것에 집중할 수 있도록 해준다. - 정해진 규약이 있어 애플리케이션을 효율적으로 관리할 수 있다.
사용하는 Framework의 규약에 맞게 코드를 작성하기 때문에, 유지 보수가 필요한 경우 더 빠르고 쉽게 문제점을 파악해 수정할 수 있다. 또한 내가 작업한 코드를 다른 사람이 수정하거나 다른 사람의 코드를 내가 수정할 경우에도 빠르게 코드를 파악할 수 있다.
단점
- 사용하고자 하는 Framework에 대한 학습이 필요하다.
- 자유롭고 유연한 개발이 어렵다.
1-2. Framework와 Library의 차이점
소프트웨어 관점에서의 Library는 애플리케이션을 개발하는 데 사용되는 일련의 데이터 및 프로그래밍 코드를 의미한다. 즉, 애플리케이션을 개발할 때 필요한 기능을 미리 구현해놓은 집합체라고 할 수 있다.
하나의 자동차를 예로 살펴보자.
Framework는 자동차의 뼈대, 다시 말하면 Frame을 의미하며 Library는 자동차의 부품이라고 할 수 있다.
자동차의 Frame을 교체하는 건 거의 불가능하지만 바퀴나 와이퍼 같은 부품은 교체가 쉽다.
이 말은 소프트웨어 관점에서도 Framework를 교체하는 일은 어렵지만, Library는 쉽게 교체가 가능하다고 해석이 가능하다.
정리하자면 Framework는 Library와 애플리케이션에 대한 제어권의 차이가 있다는 것이다.
@SpringBootApplication
@RestController
@RequestMapping(path = "/v1/message")
public class SampleApplication {
@GetMapping
public String getMessage() { // (2)
String message = "hello world";
return StringUtils.upperCase(message); // (1)
}
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
위 코드에서 명확하게 Library를 사용하는 부분은 (1)StringUtils.upperCase(message) 이다.
여기서 StringUtils 클래스는 Apache Commons Lang3 라이브러리의 유틸리티 클래스 중 하나이며 StringUtils클래스의 upperCase() 메서드를 사용하고 있음을 알 수 있다.
이렇게 개발자가 필요한 기능을 해당 라이브러리를 호출해서 사용하는 것이 Library다.
이번에는 Framework를 살펴보자.
(2)getMessage() 메서드 내부의 코드처럼 개발자가 메서드 내에 코드를 작성해두면 Spring Framework에서 개발자가 작성한 코드를 사용해 애플리케이션의 흐름을 만들어낸다.
즉, 애플리케이션의 흐름의 주도권이 개발자가 아닌 Framework에 있는 것이다.
1-3. Spring Framework란?
웹 애플리케이션 개발을 위한 Framework는 Spring 뿐만 아니라, Django, Express, Flask, Lalavel 등이 있다.
그렇다면 Spring Framework를 사용하는 이유는 무엇일까?
- POJO(Plan Old Java Object)기반의 구성
- DI(Dependency Injection) 지원
- AOP(Aspect Oriented Programming, 관점 지향 프로그래밍) 지원
- Java 언어를 사용함으로써 얻는 장점
2. Spring Framework
위 그림은 Spring 삼각형이라고 불리는데, 이 삼각형 하나로 Spring의 핵심 개념들을 모두 표현하고 있다고 해도 과언이 아니다.
지금부터 핵심 개념들을 하나씩 알아보자.
2-1. POJO
POJO란 Plain Old Java Object의 약자로 오래된 방식의 순수한 자바객체라고 할 수 있다.
POJO 프로그래밍으로 작성한 코드라고 하기 위해서는 두 가지의 기본적인 규칙은 지켜야 한다.
- Java나 Java의 스펙(사양)에 정의된 것 이외에는 다른 기술이나 규약에 얽매이지 않아야 한다.
public class User {
private String userName;
private String id;
private String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
자바에서 제공하는 기능만 사용하여 getter, setter만 가지고 있는 코드이다. Java 언어 이외에 특정한 기술에 종속되어 있지 않은 순수한 객체이기 때문에 POJO라고 부를 수 있다.
public class MessageForm extends ActionForm{ // (1)
String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
public class MessageAction extends Action{ // (2)
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
MessageForm messageForm = (MessageForm) form;
messageForm .setMessage("Hello World");
return mapping.findForward("success");
}
}
ActionForm 클래스와 Action 클래스는 Struts라는 웹 프레임워크에서 지원하는 클래스다. 이렇게 특정 기술을 상속해서 코드를 작성하게 되면 나중에 애플리케이션의 요구사항이 변경돼서 다른 기술로 변경하려면 Struts의 클래스를 명시적으로 사용했던 부분을 전부 다 일일이 제거하거나 수정해야 한다. 그리고 Java는 다중 상속을 지원하지 않기 때문에 객체지향 설계 기법을 적용하기 어려워진다.
- 특정 환경에 종속적이지 않아야 한다.
순수 Java로 작성한 애플리케이션 코드 내에서 Tomcat이 지원하는 API를 직접 가져다가 사용한다고 가정해보자. 만약 시스템 요구사항이 변경돼서 Tomcat 말고 Zetty라는 다른 Servlet Container를 사용하게 된다면 어떨까? 최악의 경우 애플리케이션을 전부 뜯어 고쳐야 될지도 모른다.
POJO 프로그래밍이 필요한 이유를 정리해보자.
- 특정 환경이나 기술에 종속적이지 않으면 재사용과 확장이 가능한 유연한 코드를 작성할 수 있다.
- 저수준 레벨의 기술과 환경에 종속적인 코드를 애플리케이션 코드에서 제거함으로써 코드가 간결해진다.
- 코드가 깔끔해지기 때문에 디버깅이 상대적으로 쉽다.
- 특정 기술이나 환경에 종속적이지 않아서 테스트 역시 단순하다.
- 객체지향적인 설계를 제한 없이 적용할 수 있다.
아래는 POJO 프로그래밍을 작성하기 위해 Spring에서 지원하는 세 가지 기술이다.
2-2. IoC
IoC(Inversion of Control)란 애플리케이션 흐름의 주도권을 Spring이 갖는다는 것이다.
public class Example2_10 {
public static void main(String[] args) {
System.out.println("Hello IoC!");
}
}
일반적으로 이 코드와 같은 Java 콘솔 애플리케이션을 실행하려면 main() 메서드가 있어야 한다.
main() 메서드가 호출된 후 System 클래스를 통해 println()을 호출한다.
이렇게 개발자가 작성한 코드를 순차적으로 실행하는 게 애플리케이션의 일반적인 제어 흐름이다.
이번에는 Java 콘솔 애플리케이션이 아니라 웹 상에서 돌아가는 Java 웹 애플리케이션의 경우를 생각해보자.

웹에서 동작하는 애플리케이션의 경우 클라이언트가 외부에서 접속해서 사용하는 서비스이기 때문에 main() 메서드가 종료되지 않아야 한다.
그런데 서블릿 컨테이너에서는 서블릿 사양(Specification)에 맞게 작성된 서블릿 클래스만 존재하지 별도의 main() 메서드가 존재하지 않는다.
main() 메서드가 없는데 어떻게 애플리케이션이 실행되는 걸까?
서블릿 컨테이너의 경우, 클라이언트의 요청이 들어올 때마다 서블릿 컨테이너 내의 컨테이너 로직(service() 메서드)이 서블릿을 직접 실행시켜 준다.
서블릿 컨테이너가 서블릿을 제어하고 있기 때문에 애플리케이션의 주도권은 서블릿 컨테이너에 있다.
바로 서블릿과 웹 애플리케이션 간에 IoC(제어의 역전)의 개념이 적용되어 있는 것이다.
Spring에서는 이 IoC의 개념이 DI(Dependency Injection)를 통해 적용되어 있다.
2-3. DI
DI(Dependency Injection)는 IoC 개념을 조금 구체화 시킨 것이라고 볼 수 있다.
DI를 의미 그대로 해석하면 의존성 주입이 되는데 이 “의존성 주입” 이라는 용어에 대해 육하원칙에 따라 단계적으로 살펴보자.
- 의존성 주입은 무엇일까?
A 클래스가 B 클래스의 기능을 사용할 때 “A 클래스는 B 클래스에 의존한다”고 표현한다.
public class MenuController {
public static void main(String[] args) {
MenuService menuService = new MenuService();
List<Menu> menuList = menuService.getMenuList();
}
}
public class MenuService {
public List<Menu> getMenuList() {
return null;
}
}
MenuController 클래스는 클라이언트의 요청을 받는 엔드포인트(Endpoint) 역할을 하고, MenuService 클래스는 MenuController 클래스가 전달 받은 클라이언트의 요청을 처리하는 역할을 하고 있다.
MenuController 클래스는 MenuService 클래스의 객체를 생성한 후, 이 객체로 getMenuList() 메서드를 호출하고 있다.
이처럼 사용하고자 하는 클래스의 객체를 생성해서 참조하게 되면 의존 관계가 성립한다.
아직까지는 의존 관계는 성립되었지만 의존성 주입은 이루어지지 않았다.
public class CafeClient {
public static void main(String[] args) {
MenuService menuService = new MenuService();
MenuController controller = new MenuController(menuService);
List<Menu> menuList = controller.getMenus();
}
}
public class MenuController {
private MenuService menuService;
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public List<Menu> getMenus() {
return menuService.getMenuList();
}
}
public class MenuService {
public List<Menu> getMenuList() {
returl null;
}
}
MenuController 클래스에서 MenuService의 객체를 직접 생성하지 않고 MenuController 생성자로 MenuService의 객체를 전달 받고 있다.
이처럼 생성자를 통해 어떤 클래스의 객체를 전달 받는 것을 “의존성 주입”이라고 한다.
생성자의 파라미터로 객체를 전달하는 것을 “외부에서 객체를 주입한다”고 표현한다.
여기에서는 CafeClient 클래스가 MenuController의 생성자 파라미터로 menuService를 전달하고 있기 때문에 객체를 주입해주는 “외부”가 되는 것이다.
만약 의존성 주입이 정확하게 이해가 되지 않는다면 딱 하나만 기억하자.
💡 클래스의 생성자로 객체를 전달 받는 코드가 있다면 의존성 주입이 이루어지는 것이다.
2. 의존성 주입이 왜 필요할까?
Java에서 생성자를 통해 객체를 전달하는 일은 아주 흔한 일이다. 즉, 의존성 주입이 필요한 건 어찌 보면 당연하다.
그런데 의존성 주입을 사용할 때 항상 염두에 두어야 하는 부분이 한 가지 있다.
현재의 클래스 내부에서 외부 클래스의 객체를 생성하기 위한 new 키워드를 쓸지 말지 여부를 결정하는 것이다.
애플리케이션 코드 내부에서 직접적으로 new 키워드를 사용할 경우 객체지향 설계의 관점에서 중요한 문제가 발생할 수 있다.
예를 들어 참조할 클래스가 바뀌는 경우 이 클래스를 사용하는 모든 클래스를 수정해야 한다.
이렇게 new 키워드를 사용해서 의존 객체를 생성할 때 클래스들 간에 강하게 결합(Tight Coupling) 되어 있다고 한다.
결론적으로 의존성 주입을 하더라도 클래스들 간의 느슨한 결합(Loose Coupling)이 필요하다.
3. 느슨한 의존성 주입은 어떻게 해야 할까?
대표적인 방법은 인터페이스를 사용하는 것이다.

public class CafeClient {
public static void main(String[] args) {
MenuService menuService = new MenuServiceStub();
MenuController controller = new MenuController(menuService);
List<Menu> menuList = controller.getMenus();
}
}
public class MenuController {
private MenuService menuService;
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public List<Menu> getMenus() {
return menuService.getMenuList();
}
}
public interface MenuService {
List<Menu> getMenuList();
}
public class MenuServiceStub implements MenuService {
public List<Menu> getMenuList() {
return List.of(
new Menu(1, "아메리카노", 2500),
new Menu(2, "카라멜 마끼아또", 4500),
new Menu(3, "바닐라 라떼", 4500)
);
}
}
3-4번 줄에서 new 로 MenuServiceStub 클래스의 객체를 생성해서 MenuService 인터페이스에 할당하고 있다.
이처럼 인터페이스 타입의 변수에 그 인터페이스의 구현 객체를 할당할 수 있는데 이를 업캐스팅(Upcasting)이라고 한다.
하지만 클래스들의 관계를 느슨하게 만들기 위해서는 new 키워드를 사용하지 않아야 되는데, 여전히 new를 사용하고 있다.
이 new 키워드는 어떻게 하면 제거하고 의존 관계를 느슨하게 만들 수 있을까?
바로 Spring이 대신 해준다.
4. Spring 기반 애플리케이션에서는 의존성 주입을 누가 해줄까?
public class CafeClient {
public static void main(String[] args) {
GenericApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
MenuController controller = context.getBean(MenuController.class);
List<Menu> menuList = controller.getMenus();
}
}
public class MenuController {
private MenuService menuService;
@Autowired
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
public List<Menu> getMenus() {
return menuService.getMenuList();
}
}
@Configuration
@ComponentScan(basePackageClasses = cafeClient.class)
public class Config {
@Bean
public MenuService getMenuService() {
return new MenuServiceStub();
}
@Bean
public MenuController getMenuController(MenuService menuService) {
return new MenuController(menuService);
}
}
3-5번 줄 Spring에서 지원하는 API 코드가 new 키워드를 대신해서 객체를 생성해주고 있다.
앞서 POJO 프로그래밍 규칙 중 “다른 기술이나 규약에 얽매이지 않아야 한다”가 있었는데 여기에서 Spring Framework에 해당하는 코드가 직접적으로 작성되어 있다.
의존성 주입의 예를 보여주기 위해 사용한 코드이기 때문이며 실제 Spring 기반의 웹 애플리케이션에는 나오지 않는다.
Config 클래스에 정의해둔 MenuController 객체를 Spring의 도움을 받아 CafeClient 클래스에 제공하고 있다.
Config 클래스 안에서 new 키워드로 객체를 생성하고 있지만 Config는 Spring Framework의 영역에 해당하는 것이고, 이 Config 클래스가 실제 애플리케이션의 핵심 로직에 관여하지 않는다.
2-4. AOP
AOP(Aspect Oriented Programming)은 관심 지향 프로그래밍이다.
애플리케이션을 개발하다 보면 애플리케이션 전반에 걸쳐 공통적으로 사용되는 기능들이 있는데, 이러한 공통 기능들에 대한 관심사를 바로 공통 관심 사항(Cross-cutting concern)이라고 한다.
그리고 흔히 말하는 비즈니스 로직, 즉 애플리케이션의 주목적을 달성하기 위한 핵심 로직에 대한 관심사를 핵심 관심 사항(Core concern)이라고 한다.
커피 전문 애플리케이션을 예로 들면, 사장이 커피 종류를 등록하는 기능과 고객이 커피를 주문하는 기능은 핵심 관심 사항에 해당된다.
애플리케이션 보안에 대한 부분은 애플리케이션 전반에 공통적으로 적용되는 기능이기 때문에 공통 관심 사항에 해당된다.
AOP는 애플리케이션의 핵심 업무 로직에서 로깅이나 보안, 트랜잭션 같은 공통 기능 로직들을 분리하는 것이다.
public class Example2_11 {
private Connection connection;
public void registerMember(Member member, Point point) throws SQLException {
connection.setAutoCommit(false); // (1)
try {
saveMember(member); // (2)
savePoint(point); // (2)
connection.commit(); // (3)
} catch (SQLException e) {
connection.rollback(); // (4)
}
}
private void saveMember(Member member) throws SQLException {
PreparedStatement psMember =
connection.prepareStatement("INSERT INTO member (email, password) VALUES (?, ?)");
psMember.setString(1, member.getEmail());
psMember.setString(2, member.getPassword());
psMember.executeUpdate();
}
private void savePoint(Point point) throws SQLException {
PreparedStatement psPoint =
connection.prepareStatement("INSERT INTO point (email, point) VALUES (?, ?)");
psPoint.setString(1, point.getEmail());
psPoint.setInt(2, point.getPoint());
psPoint.executeUpdate();
}
}
registerMember() 메서드 내에서 실제로 비즈니스 로직을 수행하는 코드는 (2)saveMember(), savePoint()이다.
(1), (3), (4)의 기능들은 (2)작업을 트랜잭션으로 묶어서 처리하기 위한 기능들이다.
문제는 이렇게 트랜잭션 처리를 하는 코드들이 애플리케이션의 다른 기능에도 중복되어 나타날 것이라는 거다.
이런 경우 중복된 코드를 공통화해서 재사용이 가능하도록 만들어야 한다.
Spring에서는 이미 이런 트랜잭션 처리 기능을 AOP를 통해 공통화 해두었다.
@Component
@Transactional // (1)
public class Example2_12 {
private Connection connection;
public void registerMember(Member member, Point point) throws SQLException {
saveMember(member);
savePoint(point);
}
private void saveMember(Member member) throws SQLException {
// Spring JDBC를 이용한 회원 정보 저장
}
private void savePoint(Point point) throws SQLException {
// Spring JDBC를 이용한 포인트 정보 저장
}
}
@Transactional 애노테이션 하나만 붙이면 이 애노테이션 정보를 활용해서 트랜잭션을 적용한다.
2-5. PSA
PSA(Portable Service Abstraction)란 추상화의 개념을 애플리케이션에서 사용하는 서비스에 적용하는 기법이다.

이 그림은 Java 콘솔 애플리케이션에서 클라이언트가 데이터베이스에 연결하기 위해 JdbcConnector를 사용하기 위한 예시다.
JdbcConnector가 애플리케이션에서 이용하는 하나의 서비스가 되는 것이다.
DbClient는 JdbcConnector 인터페이스를 통해 간접적으로 연결되어(느슨한 결합) Connection 객체를 얻는 것을 볼 수 있다.
그런데 DbClient에서 어떤 JdbcConnector 구현체를 사용하더라도 Connection을 얻는 방식은 getConnection() 메서드를 사용하는 것으로 동일하다. 즉, 일관된 방식으로 해당 서비스의 기능을 사용할 수 있다.
이처럼 애플리케이션에서 특정 서비스를 이용할 때, 접근하는 방식 자체를 일관되게 유지하면서 기술 자체를 유연하게 사용할 수 있도록 서비스의 기능을 구성하는 것을 PSA라고 한다.
PSA가 필요한 주된 이유는 애플리케이션에서 사용하는 기술이 변경되더라도 최소한의 변경만으로 변경된 요구 사항을 반영하기 위함이다.
'CodeStates_BE_44 > TIL' 카테고리의 다른 글
[Spring] Spring Framework 핵심 개념_DI (0) | 2023.04.05 |
---|---|
[DB] 데이터베이스 설계 (0) | 2023.03.29 |
[DB] SQL, NoSQL (0) | 2023.03.28 |
[네트워크] HTTP (0) | 2023.03.27 |
[네트워크] 웹 애플리케이션 작동 원리 (0) | 2023.03.24 |