스프링 - 백엔드 개발 입문 1
- -
실무에서 제대로 동작하는 웹 어플리케이션을 개발하기 위해 스프링을 학습하고 사용을 하는데,
스프링이란? java 백엔드 개발에 있어서 떼어놓을 수 없는 프레임워크이며, 정의는 아래와 같다.
>> 엔터프라이즈용 Java 애플리케이션 개발을 편하게 할 수 있게 해주는 오픈소스 경량급 애플리케이션 프레임워크
기업에서 운영하는 웹 서비스에는 비즈니스 로직이라는것이 있는데, 이 때 비즈니스 로직은 기업이 제공하는 서비스를 코드로 구현한 것으로 사용자의 요구사항을 해결하기 위한 실질적인 코드를 뜻한다. 스프링 이전에는 비즈니스 로직을 구현하기 위해 기술 자체에 대해 추가적으로 학습해야 했는데, 이는 비즈니스 로직 구현 기술이 복잡하고 어려웠기 때문이다.
하지만, 스프링을 사용하면 방법이 상대적으로 덜 복잡해지는데, 개발 초기에 기본적인 설정과 적용시킬 기술들만 잘 선택을 해준다면, 기술보다는 애플리케이션의 로직 자체에 더 집중하여 비즈니스 로직을 구현할 수 있다.
스프링은 수십개의 세부 모듈 및 수십만줄의 방대한 코드로 이루어진 프레임워크이다. 그럼에도 불구하고 스프링을 정의할 때에 경량급이라는 수식어를 사용할 수 있는데,
스프링을 정의함에 있어 경량급이라 함은 기존에 스프링 대신 사용하던 기술들과 비교하여, 스프링을 사용했을 때에 개발자가 작성해야 할 코드가 상대적으로 단순하다는 것을 표현하기 위함이다.
스프링을 정의함에 있어 경량급 애플리케이션 프레임워크라 함은, 스프링을 사용함으로써 기존 기술을 사용할 때에 불가피하게 작성해야만 했던 불필요하게 복잡한 코드를 제거하여 코드의 복잡성을 낮출 수 있음을 의미한다.
웹 개발에 있어 프레임워크란, 어떠한 목적을 쉽게 달성할 수 있도록 해당 목적과 관련된 코드의 뼈대를 미리 만들어둔 것을 의미한다.
애플리케이션 프레임워크는 애플리케이션을 개발하는 데에 있어 필요한 모든 업무 분야 및 모든 기술과 관련된 코드들의 뼈대를 제공한다.
스프링은 POJO 프로그래밍을 지향한다.
POJO란, Plain Old Java Object, 즉 순수 Java만을 통해서 생성한 객체를 의미한다.
순수 Java 만을 사용한다는 것은 Java 및 Java의 스펙에 정의된 기술만 사용한다는 의미이다.
어떤 객체가 외부의 라이브러리나 외부의 모듈을 가져와서 사용하고 있다면, 그 객체는 POJO라고 할 수 없다.
POJO는 말 그대로, 다른 기술을 사용하지 않는 순수한 Java만을 사용하여 만든 객체인 것이다.
ex: 외부 라이브러리를 import하여 라이브러리의 메서드를 사용하고 있는 객체가 있다고 가정
이 객체는 순수 Java 외에 외부 기술을 사용하고 있으므로, POJO가 아니다.
POJO는 순수 Java만을 사용하여 만든 객체이므로 특정 기술이나 환경에 종속되지 않습니다. 따라서, 외부 기술이나 규약의 변화에 얽매이지 않아, 보다 유연하게 변화와 확장에 대처할 수 있다.
POJO를 사용하여 비즈니스 로직을 구현하면 객체지향 설계를 제한없이 적용할 수 있고, 코드가 단순해져 테스트와 디버깅 또한 쉬워진다.
비즈니스 로직을 구현하는 데에 POJO를 적극적으로 활용하는 프로그래밍 패러다임을 POJO 프로그래밍이라고 한다.
POJO 프로그래밍을 위해 스프링이 지원하는 기술인 IoC/DI, AOP, PSA
1. IoC / DI (Inversion of Control / Dependency Injection, 제어의 역전 / 의존성 주입)
Java는 객체지향 언어이므로, 객체들 간의 관계를 적절히 맺어주는 것이 중요한 요소
A 인스턴스가 B 인스턴스의 메서드를 호출하고 있다면 A는 B와 의존 관계를 맺은 것이 되며, 이 둘의 관계를 “A가 B에 의존하는 관계”라고 표현 (A가 B의 기능을 가져다가 사용하고 있기 때문)
개발자가 직접 new 키워드를 사용하여 인스턴스를 생성하는 코드를 작성했기 때문에 위의 A와 B의 의존 관계는 개발자에 의해 만들어졌다.
위처럼 코드를 작성할 때에 필연적으로 발생하는 문제가 있다.
만약, A가 사용할 객체를 B가 아니라, 새롭게 C를 정의해서 사용하고자 한다면인데
아래와 같이 정의할 수 있다.
위 예제에서는 기존에 B를 사용하던 객체가 A 하나뿐이므로 간단하게 바꿔주면 되지만, 만약 기존에 B를 사용하던 객체가 A 뿐만 아니라, 수십 또는 수백개가 있다면 모든 객체의 코드를 수정해주어야 한다.
위 코드는 A는 자신이 사용할 객체를 스스로 생성하지 않고, 생성자를 통해 외부로부터 받아오고 있다.
즉, A는 자신이 사용할 객체를 알지 못하며, 그저 i에 할당된 인스턴스에 example()이라는 메서드가 존재한다는 것만 알고 있다.
누군가 A가 사용할 객체를 결정하고 생성해서 A가 인스턴스화될 때 인자로 전달해주어야만 한다.
A는 B의 것이든, C의 것이든 example() 메서드를 호출할 수 있을 것이기 때문인데, 그 '누군가'가 바로 스프링이다.
스프링을 사용하면 애플리케이션의 로직 외부에서 A가 사용할 객체를 별도로 설정할 수 있다.
개발자가 설정 클래스 파일에 A가 사용할 객체를 C로 설정해두면, 애플리케이션이 동작하면서 스프링이 설정 클래스 파일을 해석하고, 개발자가 설정해둔대로 C 객체를 생성하여 A의 생성자의 인자로 C를 전달해준다.
개발자가 아닌 스프링이 A가 사용할 객체를 생성하여 의존 관계를 맺어주는 것을 IoC(Inversion of Control, 제어의 역전)라고 하며, 그 과정에서 C를 A의 생성자를 통해 주입해주는 것을 DI(Dependency Injection, 의존성 주입)라고 한다.
2. AOP (Aspect Oriented Programming, 관심 지향 프로그래밍)
애플리케이션을 개발할 때에 구현해야 하는 기능들은 크게 공통 관심 사항과 핵심 관심 사항으로 분류할 수 있다.
핵심 관심 사항은 애플리케이션의 핵심 기능과 관련된 관심 사항으로, 커피 주문 애플리케이션을 예로 든다면 메뉴 등록하기, 주문하기, 주문 변경하기 등이 있다.
공통 관심 사항은 모든 핵심 관심 사항에 공통적으로 적용되는 관심 사항들을 의미합니다. 예를 들어, 메뉴 등록하기, 주문하기, 주문 변경하기 등 모든 핵심 관심 사항에는 로깅이나 보안 등과 관련된 기능들이 공통적으로 적용되어야만 한다.
핵심 관심 사항과 공통 관심 사항이 코드에 아래와 같이 함께 모여 있으면 필연적으로 공통 관심 사항과 관련된 코드가 중복될 수밖에 없다.
코드가 중복되어져 있는 경우, 공통 관심 사항을 수행하는 로직이 변경되면 모든 중복 코드를 찾아서 일일이 수정해주어야만 한다.
위의 예제에서 발생하는 코드의 중복이라는 문제를 해결하기 위해서는 공통 관심 사항과 관련된 기능들을 별도의 객체로 분리해낸 다음, 분리해낸 객체의 메서드를 통해 공통 관심 사항을 구현한 코드를 실행시킬 수 있도록 해야 한다.
이처럼, 애플리케이션 전반에 걸쳐 적용되는 공통 기능을 비즈니스 로직으로부터 분리해내는 것을 AOP(Aspect Oriented Programming, 관심 지향 프로그래밍)라고 한다.
3. PSA (Portable Service Abstraction, 일관된 서비스 추상화)
스프링은 Java 백엔드 개발에 있어 핵심적인 역할을 수행하는 프레임워크이며, 백엔드 개발에서 데이터베이스는 떼어놓기 어려운데,
웹 서버는 데이터베이스와 소통하며 웹 클라이언트의 요청을 처리해야 하기 때문이다.
(데이터베이스의 종류는 MySQL, Oracle, Maria DB, Mongo DB 등 실로 다양)
ex) MySQL을 사용하여 개발을 완료했는데, Maria DB로 데이터베이스를 바꿔야 하는 상황을 가정, 이 때, 각 데이터베이스마다 사용 방법이 다르다면??
>>
기존에 작성한 코드를 전부 지우고 새로 작성해야 하거나, 기존 데이터베이스와 새로운 데이터베이스 간에 사용 방법이 다른 코드를 모두 찾아서 일일이 수정 ㅜㅜ
but
스프링을 사용하면 동일한 사용방법을 유지한 채로 데이터베이스를 바꿀 수 있다!
스프링이 데이터베이스 서비스를 추상화한 인터페이스를 제공해주기 때문
스프링은 Java를 사용하여 데이터베이스에 접근하는 방법을 규정한 인터페이스를 제공하고 있으며, 이를 JDBC(Java DataBase Connectivity)라고 한다.
각 데이터베이스를 만든 회사들은 자신의 데이터베이스에 접근하는 드라이버를 Java 코드의 형태로 배포하는데, 이 드라이버에 해당하는 Java 코드의 클래스가 JDBC를 구현
JDBC를 기반으로 하여 데이터베이스 접근 코드를 작성해두면, 이후에 데이터베이스를 바꾸어도 기존에 작성한 데이터베이스 접근 로직을 그대로 사용할 수 있다.
이러한 JDBC처럼 특정 기술과 관련된 서비스를 추상화하여 일관된 방식으로 사용될 수 있도록 한 것을 PSA(Portable Service Abstraction, 일관된 서비스 추상화)라고 한다.
이제부터, 인프런 강의와 관련하여 내용 복습겸 정리를 할 것이다.
우선 스프링 프로젝트를 생성해서 실행하면 Gradle 이라는 것을 볼 수 있는데,
Gradle은 의존관계가 있는 라이브러리를 함께 다운로드 한다.
스프링 부트 라이브러리
spring-boot-starter-web
spring-boot-starter-tomcat: 톰캣 (웹서버)
spring-webmvc: 스프링 웹 MVC
spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
spring-boot
spring-core
spring-boot-starter-logging
logback, slf4j
테스트 라이브러리
spring-boot-starter-test
junit: 테스트 프레임워크
mockito: 목 라이브러리
assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
spring-test: 스프링 통합 테스트 지원
0. 환경설정(welcome page)
// resources/static/index.html
<!DOCTYPE HTML>
<html>
<head>
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
</html>
static/index.html 을 올려두면 Welcome page 기능을 제공
아래는 thymeleaf 템플릿 엔진 관련
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model) {
model.addAttribute("data", "hello!!");
return "hello";
}
}
// resources/templates/hello.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>
컨트롤러에서 리턴 값으로 문자를 반환하면 뷰 리졸버( viewResolver )가 화면을 찾아서 처리
스프링 부트 템플릿엔진 기본 viewName 매핑 ( resources:templates/ +{ViewName}+ .html )
( spring-boot-devtools 라이브러리를 추가하면, html 파일을 컴파일만 해주면 서버 재시작 없이 View 파일 변경이 가능 )
인텔리제이 컴파일 방법: build > Recomplie
빌드 및 실행
0. 콘솔로 이동
1. ./gradlew build
2. cd build/libs
3. java -jar hello-spring-0.0.1-SNAPSHOT.jar
4. 실행 확인
1. 정적 컨텐츠
// resources/static/hello-static.html
<!DOCTYPE HTML>
<html>
<head>
<title>static content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
정적 컨텐츠 입니다.
</body>
</html>
2. MVC와 템플릿 엔진
MVC: model view controller
Controller
@Controller
public class HelloController {
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello-template";
}
}
View
// resources/templates/hello-template.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>
3. API
@ResponseBody 문자 반환
@Controller
public class HelloController {
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
}
@ResponseBody를 사용하면 뷰 리졸버를 사용하지 않음, 대신 http의 body에 문자 내용을 직접 반환
@ResponseBody 객체 반환
@Controller
public class HelloController {
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
} }
}
@ResponseBody를 사용하고, 객체를 반환하면 객체가 JSON으로 변환된다.
회원 관리 예제: 백엔드 개발
예제는 데이터 저장소가 선정되지 않아, 인터페이스로 구현 클래스를 변경할 수 있도록 설계
개발 진행을 위해 초기 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
<회원 객체>
package hello.hellospring.domain;
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
} }
<회원 레포지토리 인터페이스>
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
<회원 레포지토리 메모리 구현체>
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L; //0,1,2등의 키값 생성
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
// ofNullable사용하면 store.get(id)가 null이어도 반환가능
// 이렇게 감싸서 반환하면 클라이언트에서 무엇인가를 할 수 있음(뒤에 설명)
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
public void clearStore() {
store.clear();
}
}
<회원 레포지토리 메모리 구현체 테스트>
// src/test/java 하위 폴더에 생성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
//given
Member member = new Member();
member.setName("spring");
//when
repository.save(member);
//then
Member result = repository.findById(member.getId()).get();
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
Member result = repository.findByName("spring1").get();
//then
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
List<Member> result = repository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
}
}
// @AfterEach를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행
// 여기서는 메모리 DB에 저장된 데이터를 삭제
// 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.
회원 서비스 테스트
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service //스프링이 컨테이너에 서비스로 등록
// 서비스를 통해 비즈니스 로직 구현
public class MemberService {
public final MemberRepository memberRepository;
// 외부에서 멤버 레파지토리를 넣음, 디펜던시 인젝션
@Autowired
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
public Long join(Member member) {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/**
*전체 회원 조회
*/
public List<Member> findMember() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
@BeforeEach는 각 테스트 실행 전에 호출되며, 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 맺어준다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void 회원가입(){
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외(){
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
참고
'back > spring' 카테고리의 다른 글
스프링 핵심5 - 의존관계 자동 주입 (0) | 2023.03.22 |
---|---|
스프링 핵심4 - 컴포넌트 스캔 (0) | 2023.03.22 |
스프링 핵심3 - 싱글톤 컨테이너 (0) | 2023.03.19 |
스프링 핵심2 - 스프링 컨테이너와 스프링 빈 (0) | 2023.03.19 |
스프링 핵심1 - 스프링과 객체 지향 설계 (0) | 2023.03.15 |
소중한 공감 감사합니다