Spring - 왜 Spring을 쓰는가?

Why We Use Sping and EJB

2000년 초반 자바진영에서 EJB라는 것이 존재했었다.

EnterPrise Java Beans을 말하는데 스프링이랑 JPA같은 다합쳐놓은 종합세트느낌이였다.

 

이때까지만 해도 EJB는 자바의 표준 기술이었기때문에 금융권에서 널리사용되었다.

 그래서 금융권에서 EJB 기술을 사용해서 서버를 팔아먹기에 좋은 keyword였다.

 

그 당시에 Container 기술, 설정에 의한 트랙잭션관리, 분산처리 기술(지금으로 따지면 service, dao로 분리) 와 같은

고급기술들을 사용할 수 있었다.

 

그리고 이때 ORM으로 EntityBean이라는 ORM 기술을 가지고 있었는데,

 DB에 저장된 데이터를 Java의 객체로 표현하기 위한 EJB Component였다.

지금이야 톰캣같은거 무료로 쓰지만 그때 당시에 EJB는 정말 비쌌었다. 

 

그때 당시 분명 좋은 기술이었지만 사용하기가 너무 복잡하고 어려우며 느렸었다.

EJB가 제공하는 인터페이스들을 구현해야만하고 EJB에 의존적으로 개발해야만 했다. 

그러다보니 코드도 지저분해지고 비즈니스 로직을 이해하는데도 어려워지게 되었다. 

 

이러한 EJB의 문제점들을 나열하면서 EJB없어도 충분히 고품질의 확장 가능한 Web Application을 개발할 수 있다며

expert one-on-one J2EE Design and Development라는 책을 출간했다.

 

 해당 책에서 3만줄 이상의 기반 기술 예제코드를 선보이며 BeanFactory, ApplicationContenxt, POJO, IoC, DI에 대한

개념들을 선보였다. 책이 유명해지면서 그 당시 수많은 개발자들이 책의 예제 코드를 프로젝트에 사용하기 시작했고 

유겐 휠러(Juergen Hoelelr)와 얀 카로프(Yann Caroff)가 로드 존슨에게 오픈소스 프로젝트를 하자고 제안했다.

 

지금도 스프링의 핵심 코드의 상당수를 유겔 휠러가 개발하고 있다.

이렇든 Spring 이라는 이름의 기원도 J2EE(EJB)라는 겨울을 넘어 새로운 시작이라는 뜻으로 Spring이라고 이름지어졌다.

 

Spring Release History

1. 2003년 : Spring Framework 1.0 출시 -> 각종 설정들을 XML기반으로 많이했었음 

2. 2006년 : Spring Framework 2.0 출시 -> XML 기반으로 설정할게 너무많아서 XML 편의 기능을 지원하게됨.

3. 2009년 : Spring Framework 3.0 출시 -> 본격적으로 자바코드로 설정을 할 수 있게함( 그전에도 가능하긴했으나 불편)

4. 2013년 : Spring Framework 4.0 출시 -> Java 8을 지원하게됨.

5. 2014년 : Spring Boot 1.0 출시 -> 큰 전환점으로 스프링이 다 좋은데 설정이 너무 힘들어서 이러한 설정하기도 어렵고

                  WAS에 배포할 때 세팅하는 것도 쉽지 않았음.

6. 2017년 : Spring Framework 5.0, Spring Boot 2.0 출시 -> 리액티비 프로그래밍 지원(자바에서도 non-blocking 기술을

                   가지고 개발할 수 있도록 node.js처럼)

7. 2023년 1월 현재 : Spring 6.0.4 까지 출시하였고 Spring Boot는 3.0.2 버전까지 출시하게 되었다.

 

Why Spring?

 Spring을 공부하면서 왜 Spring을 사용하는지 한마디로 정리할 수 없다면 Spring을 왜 공부하는지 되돌아 봐야 할 것이다.

Spring을 사용하는 이유는 OOP(객체지향 프로그래밍)를 하기에 편리한 도구이기 때문에 사용하는 것이다.

 

그렇다면 왜 OOP를 하기에 편리한 도구이냐?

객체 지향 프로그래밍(OOP)의 중요한 원칙인

'객체 간의 결합도를 최소화하라'를  개념을 지원해주기 때문이다.

아래는 객체간의 결합도를 최소화 하기 위해 Spring 에서 지원가는 기능이다.

 

1. 다형성을 활용한 스프링의 의존성 주입(Dependency Injection)

  Java에서 인터페이스를 사용하면 다양한 클래스가 동일한 인터페이스를 구현할 수 있으며,

이로 인해 다형성이 지원된다. 스프링에서는 이러한 다형성을 활용하여 의존성 주입(Dependency Injection)을 가능한다.

  예를 들어, "PaymentService"라는 인터페이스가 있고 "CreditCardPayment"와 "PaypalPayment"라는 두 개의 클래스가 이를 구현한다고 가정하면  각각의 클래스는 결제 방법에 따라 다른 로직을 가진다.

public interface PaymentService {
    void pay();
}

public class CreditCardPayment implements PaymentService {
    @Override
    public void pay() {
        System.out.println("Paying with credit card");
    }
}

public class PaypalPayment implements PaymentService {
    @Override
    public void pay() {
        System.out.println("Paying with PayPal");
    }
}

 


여기서 스프링은 의존성 주입 기능으로 @Autowired 어노테이션을 통해 적절한 객체를 자동으로 연결해준다.

@Service
public class OnlineStore {

   private final PaymentService paymentService;

   @Autowired
   public OnlineStore(PaymentService paymentService) {
       this.paymentService = paymentService;
   }

   public void makePayment() {
       paymentService.pay();
   }
}

 

2. 약한 결합(Loose Coupling)과 모듈화

 위 1번에서 개념을 좀 더 확장하면, 스프링을 활용하여  클래스들이 직접적으로 의존하지 않고 인터페이스에 의존함으로서 변경에 대응하기 쉬워지며, 각각의 부분(partition)이 독립적으로 작동할 수 있게 코드를 작성할 수 있다.

 

예를 들어, 데이터베이스와 연결하는 DAO(Data Access Object) 클래스를 예제로 보자.

 

먼저, UserDao 인터페이스를 정의한다.

public interface UserDao {
    User getUser(int id);
}

 


그 다음으로 이 인터페이스를 구현하는 MySqlUserDao와 PostgresUserDao 클래스 두 가지 버전이 있다고 가정하자.

public class PostgresUserDao implements UserDao {
    @Override
    public User getUser(int id) {
        // PostgreSQL-specific code to fetch a user...
    }
}


 

이제 UserService라는 서비스 계층에서는 특정 DAO 구현체(MySqlUserDao 또는 PostgreUserDao)에 의존하지 않고, 대신 UserDao 인터페이스에 의존하도록 만들면 나중에 다른 종류의 데이터베이스로 전환하거나 다른 DAO 구현체로 교체할 때 UserService 코드는 전혀 변경되지 않아도된다.

@Service
public class UserService {

   private final UserDao userDao;

   @Autowired
   public UserService(UserDao userDao) {
       this.userDao = userDao;
   }

   public User findUser(int id) {
       return userDao.getUser(id);
   }
}


여기서 스프링 프레임워크의 DI(Dependency Injection) 기능은 적절한 UserDao 구현체를 자동으로 주입기 때문에 어떤 구현체가 주입될지는 스프링 설정 파일이나 어노테이션 등을 통해 설정된다.

 

만약 스프링이 자동으로 주입해주지 않는다면?

public class UserService {

   private final UserDao userDao;

   public UserService() {
       this.userDao = new MySqlUserDao(); // UserDao의 MySql 구현체를 직접 생성
   }

   public User findUser(int id) {
       return userDao.getUser(id);
   }
}

 

스프링의 DI 기능이 없다면 개발자는 위와 같이 MySQLUserDao 라는 구현체를 직접 주입해줘야한다.

이렇게 되면 만약 다른 PostgreSQL DB로 바꾸게 되면   

 

 public UserService() {
       this.userDao = new PostgreSQLDao (); // 
   }

로 수정해주어야 한다.

 

하지만 스프링으로 구현하면 해당 수정사항이 필요없고 @Primary나 @Qualifier 어노테이션을 사용하여 여러 구현체 중 어떤 것을 주입할지 결정할 수 있다.

 

@Repository
public class MySqlUserDao implements UserDao {
    @Override
    public User getUser(int id) {

    }
}

@Repository
public class PostgresUserDao implements UserDao {
    @Override
    public User getUser(int id) {

    }
}



//@Primary를 사용한 경우

@Repository
@Primary  // 이 어노테이션이 붙은 구현체가 우선적으로 주입됩니다.
public class MySqlUserDao implements UserDao { ... }

@Repository
public class PostgresUserDao implements UserDao { ... }



//@Qualifier를 사용한 경우

@Service 
public class UserService {

   private final UserDao userDao;

   @Autowired 
   public UserService(@Qualifier("postgresUserDao") UserDao userDao) {  // "postgresUserDao"라는 이름의 빈을 주입합니다.
       this.userDao = userDao;
   }

   ...
}

 

 

 

위 예제에서 알 수 있듯이, 약한 결합과 모듈화 원칙은 코드의 유연성과 재사용성을 높여주며 유지보수가 용이하게 만들어준다.

 

[ 예제 말고실무 코드 ]

1.UserRepository

package com.example.happyusf.Mappers;

import com.example.happyusf.Domain.MessageDTO;
import com.example.happyusf.Domain.UserDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface UserRepository {

      @Select("SELECT * FROM user_info WHERE user_id = #{user_id}")
      UserDTO findByUserID(String user_id);

      @Insert("INSERT INTO user_info (user_id, password, phone_number, birth_date, email, code_user_grade) VALUES (#{user_id}, #{password}, #{phone_number}, #{birth_date}, #{email}, 'N0')")
      int joinNewUser(UserDTO userDTO);

      @Select("SELECT user_id FROM user_info WHERE phone_number = #{to}")
      UserDTO findUserIdByMobile(MessageDTO messageDTO);

      @Update("UPDATE user_info SET password =#{password} WHERE phone_number = #{phone_number}")
      int resetPassword(UserDTO userDTO);

}

 

2. UserRepositoryService

package com.example.happyusf.Service.UserService;


import com.example.happyusf.Domain.MessageDTO;
import com.example.happyusf.Domain.UserDTO;
import com.example.happyusf.Mappers.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UserRepositoryService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    @Autowired
    public UserRepositoryService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public int joinNewUser(UserDTO userDTO){

        // ID 중복검사
        UserDTO alreadyExistingUser = userRepository.findByUserID(userDTO.getUser_id());
        if(alreadyExistingUser != null){
            throw new IllegalArgumentException("이미 사용중인 아이디입니다.");
        }

        // 패스워드 암호화 저장
        userDTO.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        userDTO.setPhone_number(userDTO.getPhone_number().replaceAll("-", ""));
        return userRepository.joinNewUser(userDTO);

    }

    public UserDTO findIdByMobile(MessageDTO messageDTO){
        return userRepository.findUserIdByMobile(messageDTO);
    }

    public int resetPassword(UserDTO userDTO){
        userDTO.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        return userRepository.resetPassword(userDTO);
    }

}

UserRepository 인터페이스에 의존하고 있으며, 이는 데이터베이스와 상호작용하는 방식을 추상화한 것이다.

따라서 UserRepository 인터페이스의 구현이 변경되더라도  ( 예시 : MySQL에서 PostgreSQL로 변경),

UserRepositoryService 클래스는 수정할 필요가 없다. 

 

 

3. 유닛 테스트 용이

인터페이스와 스프링은 유닛 테스트를 쉽게 만들어준다.

인터페이스를 구현한 가짜 객체(mock object)를 생성하여 실제 코드의 동작을 모방하기 때문이다.

스프링에서는 이러한 가짜 객체를 사용하여 의존성 주입을 통해 유닛 테스트에 필요한 환경을 구성할 수 있다.

 

@RunWith(SpringRunner.class)
public class OnlineStoreTest {

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OnlineStore onlineStore;

    @Test
    public void testMakePayment() {
        onlineStore.makePayment();
        verify(paymentService, times(1)).pay();
    }
}


위 예제코드를 보면 Mockito 라이브러리를 사용하여 PaymentService의 가짜 객체를 생성하고, OnlineStore의 의존성으로 주입하고 있다.  이렇게 함으로써 실제 PaymentService 구현체 없이도 OnlineStore의 메소드들을 테스트할 수 있다.

 

 

 

그 외

1. AOP(Aspect-Oriented Programming)

AOP를 통해, 로깅, 보안 등과 같은 공통적인 기능(cross-cutting concerns)을 비즈니스 로직으로부터 분리할 수 있습니다. 이는 코드의 모듈화를 증진시키고 가독성을 향상시킵니다.


2. 트랜잭션 관리

Spring은 선언적인 방식으로 트랜잭션 관리를 제공하므로 개발자는 복잡한 트랜잭션 API를 직접 다루지 않아도 된다.

 

3. 스프링 MVC

스프링 웹 MVC는 강력하고 사용하기 쉬운 웹 프레임워크로, RESTful 웹 서비스 개발에도 용이하다.


4. 오픈소스 커뮤니티와 지원

사실 개발할 떄 오픈소스와 커뮤니티의 지원여부가 가장 중요하다고 본다. 그런데 이러한 Spring Framework는 정말로 막대하게 크고 활발한 오픈소스 커뮤니티에 의해 지원되며, 꾸준한 업데이트와 버그 수정이 이루어지고 있다.


5. 다양한 서드파티 라이브러리와의 호환

Hibernate, JPA ,MyBatis, Spring Data
Quartz Scheduler, Apache Kafka, Elasticsearch, Thymeleaf
Spring Security , Spring Security OAuth2, Spring Cloud, Spring HATEOAS

 

이외에도 다양한 서드파티 라이브러리와 호환되기 때문에 Spring은 실로 강력한 도구이다.

'Spring' 카테고리의 다른 글

Spring - Bean과 ioc에 대해서 (예제 코드)  (0) 2022.01.11
Spring - DI (의존성 주입) 이란 무엇인가?  (0) 2022.01.11
Spring - ObjectMapper  (0) 2022.01.11
URI vs URL 개념정리  (0) 2022.01.06
REST API란 무엇인가?  (0) 2022.01.06