본문 바로가기
IT/java

스프링 복습 ( Spring4 on JDK25 ) - 환경구성, CRUD, JUnit

by 가능성1g 2026. 4. 30.
반응형

* 개발 구성

sts-5.1.1

jdk-25 ( Oracle )

maven ( 행내 보안 특성상 선택 )

 

* 데모 생성 

spring 4.0.6

Spring Web 만 추가하여 생성

아직도 빨갛게? 표시되는 https://maven.apache.org....부분만 http 로 변경함

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>4.0.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sample01</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sample01</name>
	<description/>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>25</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

이대로 실행하면, 정상실행은 되지만 Whitelabel Error Page 가 뜹니다.

찝찝하니, 파일 하나만 추가해서 헬로우 월드를 보도록 합니다.

 

1. static 페이지로 페이지 보기

resources/static/index.html 파일을 만듭니다.

<!DOCTYPE html>
<html>
	<head>
		<title>Hello</title>
	</head>
	<body>
		<p>Hello World!</p>
	</body>
</html>

 

http://localhost:8080/index.html

 

로 접속하면 됩니다.

 

2. Get API 만들기

com.example.demo 에 신규 클래스를 추가 합니다.

TestController.java 라고 이름을 정합니다.

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
	@GetMapping("/test")
	public String test() {
		return "Hello, world!";
	}
}

 

이렇게 하고, 리스타트를 합니다. 왼쪽 하단에 재시작 버튼을 눌러 주면 됩니다.

 

http://localhost:8080/test 로 접속해서 확인합니다. 

 

3. 간단한 DB 생성 / 접속 사용

 

jpa, h2 database 와 lombok 사용을 위해 pom.xml 을 수정합니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>4.0.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sample01</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sample01</name>
	<description/>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>25</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc-test</artifactId>
			<scope>test</scope>
		</dependency>
		
	    <!-- implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -->
	    <dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-starter-data-jpa</artifactId>
	    </dependency>
		
		 <!-- runtimeOnly 'com.h2database:h2' -->
	    <dependency>
	        <groupId>com.h2database</groupId>
	        <artifactId>h2</artifactId>
	        <scope>runtime</scope>
	    </dependency>
	
	    <!-- compileOnly 'org.projectlombok:lombok' -->
	    <dependency>
	        <groupId>org.projectlombok</groupId>
	        <artifactId>lombok</artifactId>
	        <optional>true</optional>
	    </dependency>
	</dependencies>

	<build>
	    <plugins>
	        <plugin>
	            <groupId>org.apache.maven.plugins</groupId>
	            <artifactId>maven-compiler-plugin</artifactId>
	            <configuration>
	                <annotationProcessorPaths>
	                    <path>
	                        <groupId>org.projectlombok</groupId>
	                        <artifactId>lombok</artifactId>
	                    </path>
	                </annotationProcessorPaths>
	            </configuration>
	        </plugin>
	        <plugin>
	            <groupId>org.springframework.boot</groupId>
	            <artifactId>spring-boot-maven-plugin</artifactId>
	            <configuration>
	                <excludes>
	                    <exclude>
	                        <groupId>org.projectlombok</groupId>
	                        <artifactId>lombok</artifactId>
	                    </exclude>
	                </excludes>
	            </configuration>
	        </plugin>
	    </plugins>
	</build>

</project>

 

Member.java (class) 생성 -> JPA 를 이용한 컬럼생성 및 Object 관련입니다.

package com.example.demo;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access= AccessLevel.PROTECTED)
public class Member {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="id", updatable= false)
	private long id;
	
	@Column(name="name", nullable = false)
	private String name;
	
	public Member(String name) {
		this.name = name;
	}
}

 

MemberRepository.java (interface) 생성 JPA 를 이용해서 기본 CRUD 함수를 생성해 줍니다.

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

}

 

 

TestService.java (class) 를 생성해서 서비스 계층을 만듭니다. 리스트 조회 서비스를 MemberRepository를 이용해서 사용합니다. 스프링의 Auwowired 를 이용해 DI 가 자동으로 이루어 지게 됩니다.

package com.example.demo;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TestService {
	
	@Autowired
	MemberRepository memberRepository;
	
	public List<Member> getAllMembers() {
		return memberRepository.findAll();
	}

}

 

TestController.java (class) 를 수정합니다.

package com.example.demo;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
	
	@Autowired
	TestService testService;
	
	@GetMapping("/test")
	public List<Member> getAllMembers() {
		List<Member> members = testService.getAllMembers();
		return members;
	}
}

 

application.properties 파일을 아래와 같이 수정합니다.

책에서는 yml 파일로 하라고 했지만, sts 에서는 공백<-> 탭 때문에 오류 발생하니 이 포맷이 차라리 낫네요.

spring.application.name=sample01
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true

 

resources/data.sql 파일을 생성합니다.

h2 database 가 메모리 형태라서 생성될때마다 사라지니 데이터를 넣어주는 역할을 합니다.

INSERT INTO member(id, name) VALUES (1, 'name 1')
INSERT INTO member(id, name) VALUES (2, 'name 2')
INSERT INTO member(id, name) VALUES (3, 'name 3')

 

그리고 그냥 실행!

하니 오류가 발생하네요.  이유는 lombok annotation 관련이 적용이 안되는거 같습니다.

maven 명령을 통해서 clean 후 재컴파일을 하면 잘됩니다. 

실행되는 로그를 보면, properties 에 설정한대로 실행되는 쿼리들이 보입니다.

 

 

아래 명령을 통해서 테스트 하면 데이터가 JSON 형태로 나오는게 보입니다.

C:\Users\1410857>curl http://localhost:8080/test
[{"name":"name 1","id":1},{"name":"name 2","id":2},{"name":"name 3","id":3}]
C:\Users\1410857>

 

4. TDD 개발을 위한 테스트 만들기 복습

JUnit4 를 이용한 단위테스트 만들기 연습입니다.

java/QuizController.java 를 만듭니다.

package com.example.demo;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class QuizController {

	@GetMapping("/quiz")
	public ResponseEntity<String> quiz(@RequestParam("code") int code ) {
		switch(code) {
			case 1:
				return ResponseEntity.created(null).body("Created!");
			case 2:
				return ResponseEntity.badRequest().body("Bad Request!");
			default:
				return ResponseEntity.ok().body("OK!");
		}
	}
	
	@PostMapping("/quiz")
	public ResponseEntity<String> quiz2(@RequestBody Code code) {
		
		switch(code.value()) {
			case 1:
				return ResponseEntity.status(403).body("Forbidden!");
			default:
				return ResponseEntity.ok().body("Ok!");
		}
	}
}

record Code(int value) {};

 

GET, POST 그리고 파라미터에 따라 응답을 다르게 주는 로직입니다. 

해당 소스파일에서 오른쪽 클릭 후, New > JUnit Test Case 를 선택하면 자동으로 QuizControllerTest.java 라는 파일을 만들어 줍니다. 아래 내용을 입력합니다.

package com.example.demo;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import tools.jackson.databind.ObjectMapper;

@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {

	@Autowired
	protected MockMvc mockMvc;
	
	@Autowired
	private WebApplicationContext context;
	
	@Autowired
	private ObjectMapper objectMapper;
	
	@BeforeEach
	public void mockMvcSetUp() {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
	}
	
	@DisplayName("quiz(): GET /quiz?code=1 이면 응답코드는 201=Created, 응답 본문은 Created!를 리턴한다.")
	@Test
	public void getQuiz1() throws Exception {
		//given
		final String url = "/quiz";
		
		//when
		final ResultActions result = mockMvc.perform(get(url)
				.param("code", "1"));
		
		//then
		result
				.andExpect(status().isCreated())
				.andExpect(content().string("Created!"));
				
	}
	
	@DisplayName("quiz(): GET /quiz?code=2 이면 응답코드는 400=Bad, 응답 본문은 Bad Request!를 리턴한다.")
	@Test
	public void getQuiz2() throws Exception {
		//given
		final String url = "/quiz";
		
		//when
		final ResultActions result = mockMvc.perform(get(url)
				.param("code", "2"));
		
		//then
		result
				.andExpect(status().isBadRequest())
				.andExpect(content().string("Bad Request!"));
				
	}
	
	@DisplayName("quiz(): POST /quiz?code=1 이면 응답코드는 403=Forbidden, 응답 본문은 Forbidden!를 리턴한다.")
	@Test
	public void postQuiz1() throws Exception {
		//given
		final String url = "/quiz";
		
		//when
		final ResultActions result = mockMvc.perform(post(url)
				.contentType(MediaType.APPLICATION_JSON)
				.content(objectMapper.writeValueAsString(new Code(1)))
				);
		
		//then
		result
				.andExpect(status().isForbidden())
				.andExpect(content().string("Forbidden!"));
				
	}
	
	@DisplayName("quiz(): POST /quiz?code=13 이면 응답코드는 200=Ok, 응답 본문은 Ok를 리턴한다.")
	@Test
	public void postQuiz13() throws Exception {
		//given
		final String url = "/quiz";
		
		//when
		final ResultActions result = mockMvc.perform(post(url)
				.contentType(MediaType.APPLICATION_JSON)
				.content(objectMapper.writeValueAsString(new Code(13)))
				);
		
		//then
		result
				.andExpect(status().isOk())
				.andExpect(content().string("Ok!"));
				
	}

}

 

** 주요 포인트 **

스프링부트 테스트를 위한 어노테이션들

@SpringBootTest

@AutoConfigureMockMvc

GET , POST 호출을 위한 함수와 각각 파라미터를 주는 방법 ( POST 호출시 GET방식으로 하면 400 오류 발생함 )

 

QuizControllerTest.java 파일에서 오른쪽 클릭후, Run as > JUnit Test 로 실행해서 정상을 확인하면 됩니다!

 

4.1 TDD로 JPA 테스트 하기

JPA 와 관련이 깊은 Repository 의 Test 소스를 생성합니다.

MemberRepository.java 에서 오른쪽 클릭 후, New > JUnit TestCase 로 테스트 파일을 생성합니다.

자동으로 MemberRepositoryTest.java 파일이 생성됩니다.

아래와 같이 수정해 봅시다!

package com.example.demo;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;

@DataJpaTest
class MemberRepositoryTest {
	
	@Autowired
	MemberRepository memberRepository;
	
	@AfterEach
	public void cleanUp() {
		memberRepository.deleteAll();
	}
	

	@Sql("/insert-members.sql")
	@Test
	void getAllMembers() {
		//when
		List<Member> members = memberRepository.findAll();
		
		//then
		assertThat(members.size()).isEqualTo(3);
		
		//when
		Member member = memberRepository.findById(2L).get();
		
		//then
		assertThat(member.getName()).isEqualTo("B");
		
		//when
		Member memberByName = memberRepository.findByName("C").get();
		
		//then
		assertThat(memberByName.getId()).isEqualTo(3);
	}
	
	@Test
	void saveMember() {
		//given
		Member member = new Member("A");
		
		//when
		memberRepository.save(member);
		
		//then
		assertThat(memberRepository.findById(1L).get().getName()).isEqualTo("A");
	}
	
	@Test
	void saveMembers() {
		//given
		List<Member> members = List.of(
				new Member("B"),
				new Member("C")
				);
		
		//when
		memberRepository.flush();
		memberRepository.saveAll(members);
		
		//then
		assertThat(memberRepository.findAll().size()).isEqualTo(2);
		
	}
	
	@Sql("/insert-members.sql")
	@Test
	void deleteMemberById() {
		//when
		memberRepository.deleteById(2L);
		
		//then
		assertThat(memberRepository.findById(2L).isEmpty()).isTrue();
	}
	
	@Sql("/insert-members.sql")
	@Test
	void deleteAll() {
		//when
		memberRepository.deleteAll();
		
		//then
		assertThat(memberRepository.findAll().size()).isZero();
	}
	
	@Sql("/insert-members.sql")
	@Test
	void update() {
		//given
		Member member = memberRepository.findById(2L).get();
		
		//when
		member.changeName("BC");
		
		//then
		assertThat(memberRepository.findById(2L).get().getName()).isEqualTo("BC");
	}
}

 

** 주요포인트 **

JPA 테스트용 파일은

@DataJpaTest 어노테이션만 붙여주면됩니다.

@Sql 로 테스트 실행전에 실행할 쿼리를 지정해 줄 수 있습니다.

@AfterEach 를 이용해, 테스트가 끝날때마다 모든 데이터를 지웠습니다.

 

 

테스트에 사용하는 insert-members.sql 파일을 아래와 같이 생성합니다. ( 위치는 resources/insert-members.sql )

INSERT INTO member(id, name) VALUES (1, 'A')
INSERT INTO member(id, name) VALUES (2, 'B')
INSERT INTO member(id, name) VALUES (3, 'C')
반응형