파트스터디에서 다시 내 세미나 시간이 돌아왔다.
다른 선배분들은 되게 멋진 주제를 가지고 발표를 하시는데,
나는 뭐 CS 지식도 적고,
그렇다고 전공과목에서 기웃기웃할 수 있는 기회도 없고,
요즘 한창 공부 중인 JVM와 컴파일러 쪽 지식과 연결하자니 백엔드, 네트워크와는 거리가 다소 먼 분야고,
네트워크 기초를 다루기에는 너무 내용이 쉽고.
암튼 그래서 나와 같은 저학년 주니어 개발자가 할 수 있는 질문을 생각해보았다.
스프링... 왜 쓰세요?
나와 같은 저학년 주니어 개발자에게 저 질문을 해본다면 제대로 대답할 수 있는 사람이 있을까? 그래서 저 질문에 대한 답을 해보고자한다.
위의 질문은 크게 두 가지로 나눌 수 있다.
1. 왜 자바와 그 프레임워크인 스프링을 쓰신 거에요?
자바스크립트 NextJS, 파이썬 Django 처럼 다른 백엔드 프레임워크나 라이브러리들도 많이 있는데 왜 하필 자바를 골랐는가?라고하면, 이거는 좀 개인차의 영역이어서 쉽게 대답할 수 있지 않을까싶다. 왜 자바를 고르셨어요? 와 비슷한 질문이기 때문.
https://color-it.tistory.com/1
[컬러잇 자바] #1. 왜 자바를 배워야할까?
※[컬러잇 자바] 시리즈는 "왜?"라는 질문을 중심으로 자바의 기초문법에서 고급 자바기술까지 알아보는 컬러잇 개발블로그 시리즈입니다. 컬러잇 자바시리즈 1편. 그 시작으로는 왜 자바를 알
color-it.tistory.com
왜 자바를 써야하는지는 컬러잇 개발블로그에서 다뤄봤다. 우리 컬러잇도 많은 관심부탁해요 주 1회 업로드
솔직히 개인에게만 맞다면야 파이썬 Django를 사용하나 자바스크립트의 NextJS를 사용하나 개인의 자유라고본다. 단지 내가 여러 이유로 자바를 골랐을 뿐.
2. 왜 스프링을 사용해서 개발하신거에요?
이 이유에 대해서 본격적으로 대답할 시간이다. 내가 찍먹이긴한데 예전에 자바스크립트 ajax를 통해 개발을 한 번 해보고 리액트를 써보는 이유를 조금 고민해본 적 있는데, 이번에도 순수자바와 그 라이브러리들을 통해 스프링을 사용하는 이유를 알아보고자한다.
순수자바로 개발하기
이번에는 뭐 큰 프로젝트도 아니고 순수자바로도 개발해봐야하기 때문에 간단한 로그인과 회원가입 기능만 개발하기로한다.
1. 서버열기
스프링에서는 Tomcat 내장 서버를 통해서 서버를 열고 관리했지만, 순수자바에서는 서버도 직접 열어야한다. 이 때 사용되는 것은 HttpServer
public class HTTPServletLauncher {
private HttpServer server = null;
public HTTPServletLauncher(String host, int port) throws IOException {
createServer(host, port);
}
private void createServer(String host, int port) throws IOException {
this.server = HttpServer.create(new InetSocketAddress(host, port), 0);
}
public void start(){
server.start();
}
public void stop(int delay){
server.stop(delay);
}
}
public class HTTPRequestListener implements HttpHandler {
public void handle(HttpExchange exchange) throws IOException {
//로그인 기능
if(exchange.getRequestURI().toString().startsWith("/login")){
if (exchange.getRequestMethod().equals("POST")) {
//로그인 기능 수행
} else {
//405 에러
}
}
//회원가입 기능
if(exchange.getRequestURI().toString().startsWith("/signup")){
if(exchange.getRequestMethod().equals("POST")) {
//회원가입 기능 수행
} else {
//405 에러
}
}
}
}
이 때 사용할 수 있는 라이브러리 중에 HttpServer라는 좋은 라이브러리가 있다. 대략적인 기능만 알아보자면, HttpServer.create(String host, int posrt)를 통해서 HttpServer가 서버를 연다. 그러면 그 서버를 통해서 요청을 받을 수 있게되는 것이고 HttpRequestListener 클래스에서 handle() 메서드를 통해 이를 처리한다.
HttpServer 클래스는 HTTP 요청이 들어오면 HTTP 요청에 담긴 여러 메타데이터들 중에서 실제로 유저가 보낸 body값만 빼내서 HttpExchange 클래스로 넘겨주는 역할을 수행한다.
if(exchange.getRequestURI().toString().startsWith("/login")){
if (exchange.getRequestMethod().equals("POST")) {
InputStream is = exchange.getRequestBody();
} else {
//405 에러
}
}
요청을 처리하는 부분에 있어서는 exchange.getRequestBody(); 메서드를 사용해서 InputStream으로 처리할 수 있다. 이제, 여기가 스프링에서는 대략적으로 컨트롤러 부분인 것이고, 서비스 계층으로 넘기기만하면 된다.
그런데 문제는 서비스 계층에서는 HTTP 요청의 body를 DTO로 받는데, exchange.getRequestBody()를 통해 얻은 값은 InputStream이다. ObjectMapper가 있었다면 ObjectMapper로 한 번에 형변환할 수 있었겠지만, 순수자바에는 ObjectMapper가 없다. 그래서 InputStream → String → DTO 를 통해 우회하는 형변환을 진행해야한다.
https://developer-talk.tistory.com/681#google_vignette
[Java]InputStream을 문자열로 변환하는 방법
InputStream을 문자열로 변환하는 방법 이번 포스팅은 InputStream을 문자열로 변환할 수 있는 몇 가지 방법을 소개합니다. Java 1.8 버전에서 사용할 수 있습니다. InputStream 클래스 InputStream 클래스는 바
developer-talk.tistory.com
private String HttpExchangeConverter(HttpExchange exchange) throws IOException {
InputStream is = exchange.getRequestBody();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
InputStream에서 String으로 바꾸는 것은 BufferedReader를 사용하면되므로 위와같이 컨버터를 따로 만들어주면 되고
{
"userid": "hihi",
"password": "dd",
"name": "colorit"
}
그러면 다음과 같은 결과가 나온다. 이것을 split() 메서드를 사용해서 파싱하면 된다. 각 메서드나 기능마다 적용되어야하는 DTO가 다르므로 이 기능은 컨버터에서 일괄적으로 처리하지 못한다.
String UserRequestString = HttpExchangeConverter(exchange);
String userId = UserRequestString.split("\\{")[1].split(":")[1].split(",")[0];
String password = UserRequestString.split("\\{")[1].split(":")[2].split(",")[0];
LoginRequestDTO loginRequestDTO = new LoginRequestDTO(userId, password);
System.out.println(userId + password);
이렇게 문자열 파싱을 통해서 DTO로 바꿔줄 수 있다.
원래라면 lombok의 @Builder, @NoArgsConstructer @AllArgsConstructer 어노테이션을 사용했지만 이것을 사용할 수 없기 때문에 기초적인 게터세터와 생성자를 통해서 DTO를 생성했다.
2. DB 구현하기
이제 HTTP 요청을 받을 수 있으므로 회원가입과 로그인 기능만 구현하면 된다. 이 기능들은 스프링에서 개발한 것에서 조금씩만 수정하면되지만, 가장 중요한 것은 DB.
스프링 JPA를 사용하면 코드도 짤 필요도 없이 메서드만 만들어서 호출하면 되지만, 순수자바에서는 DB를 열고, 그 DB를 저장할 SQL 코드도 짜야하고, 트랜잭션 관리도 해야한다.
private static final String URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1";
private static final String USER = "sa";
private static final String PASSWORD = "";
public static void main(String[] args) {
try {
//DB연결
Connection con = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("Connected to database successfully");
String createTableSql = "CREATE TABLE IF NOT EXISTS members (id INT PRIMARY KEY AUTO_INCREMENT, USERID VARCHAR(255), PASSWORD VARCHAR(255), NAME VARCHAR(255))";
try (Statement stmt = con.createStatement()) {
stmt.execute(createTableSql);
System.out.println("Table 생성 완료");
}
//서버연결
HTTPServletLauncher httpServletLauncher = new HTTPServletLauncher("localhost", 8080);
HTTPRequestListener httpRequestListener = new HTTPRequestListener(con);
httpServletLauncher.registerHandler("/login", httpRequestListener);
httpServletLauncher.registerHandler("/signup", httpRequestListener);
httpServletLauncher.start();
} catch (IOException e) {
e.printStackTrace();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
H2DB를 사용하기 때문에 H2DB 드라이버를 다운 받은 후에 main 함수에서 연결해주면 된다. SQL 라이브러리를 사용했다. 스프링을 사용했다면 컨테이너를 통해서 알아서 Connection을 주입시켰겠지만 스프링을 사용하지 않았기 때문에 생성자로 DB Connection을 만들고 계속해서 생성자로 넘겨주어야한다.
public boolean existsByUseridAndPassword(String userid, String password) throws IOException {
String sql = "SELECT * FROM members WHERE USERID = ? AND PASSWORD = ?";
try (PreparedStatement preparedStatement = con.prepareStatement(sql)){
preparedStatement.setString(1, userid);
preparedStatement.setString(2, password);
try (ResultSet resultSet = preparedStatement.executeQuery()){
if (resultSet.next()) {
return true;
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return false;
}
이 Connection을 통해서 Repository 계층에서는 일반적인 JDBC처럼 개발하면된다.
만약 트랜잭션처리를 해야한다면, JDBC는 DB에 접근해서 데이터를 수정하면 커밋이 자동으로 진행되는데, 이걸 수동으로 바꾼 후에 오류가 발생하면 예외처리로 롤백시킬 수 있다.
public void save(Member member) throws IOException {
try {
con.setAutoCommit(false);
saveMember(member);
con.commit();
} catch (SQLException e) {
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} catch (IOException e){
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
con.setAutoCommit(true);
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
private void saveMember(Member member) throws IOException, SQLException {
String userId = member.getUserid();
String password = member.getPassword();
String name = member.getName();
String sql = "INSERT INTO members (USERID, PASSWORD, NAME) VALUES (?, ?, ?)";
try (PreparedStatement preparedStatement = con.prepareStatement(sql)){
preparedStatement.setString(1, userId);
preparedStatement.setString(2, password);
preparedStatement.setString(3, name);
preparedStatement.executeUpdate();
System.out.println("saving member success");
} catch (SQLException e) {
System.out.println("error in saving member");
throw e;
}
}
위와같이 처리할 수 있다. save와 setAutoCommit, commit 등의 SQL 관련 메서드들은 모두 에러를 일으킬 수 있어서 예외처리를 여러 번 해야하기 때문에 try-catch문이 이리저리 얽혀있다. Autocloseable의 구현체라면 TWR을 사용해서 코드를 간결화할 수 있겠지만, 위 경우는 이에 해당하지 않기 때문에 여러 번 try-catch를 사용해야한다. 복잡하지요

그래서 대략적으로 프로그램은 위와 같이 실행된다. (DTO는 제외함)
먼저, 서버를 열어야하므로, main에서 HTTPServerLauncher의 createServer() 메서드를 호출해 서버를 만들고 DB를 연결한다. 그런다음에 HTTPServerLauncher의 registerHadnler() 메서드를 호출해서 필요한 URL들을 등록한 다음에 start() 메서드를 호출해 서버를 열어준다.
그리고 나중에 요청이 들어오면 HTTPRequestListener 가 이 요청을 받아서 handle() 메서드를 통해 처리한다. 가령 URL이 /login인 요청일 경우에는 HTTP Method부터 분석한 다음에, LoginService 클래스의 login() 메서드를 호출해 로그인 처리를 하는 식으로 요청이 처리된다.
중요한 것은, login() 메서드가 종료되면 응답메시지를 반환하는데, HTTPRequestListener는 이 응답메시지를 받아서 HTTPReseponseSender 클래스의 sendResponse() 메서드를 호출해 최종적으로 HTTP 응답을 반환하게된다.
왜 스프링을 쓸까요?
1. "비즈니스 로직"에 집중할 수 있다.
프레임워크와 라이브러리를 배울 때 처음 배운 내용 중 하나인데,
스프링에서는 자동으로 톰캣서버도 연결해주고, @RequsetMapping 어노테이션 한 개면 HTTP 요청을 처리할 수 있고, DB도 application.properties에 설정만 등록해두면 알아서 DB도 매핑될 수 있었다. 하지만 순수자바에서 이를 개발하면 모든 과정을 직접 처리해야했다.
public static void main(String[] args) {
try {
//DB연결
Connection con = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("Connected to database successfully");
String createTableSql = "CREATE TABLE IF NOT EXISTS members (id INT PRIMARY KEY AUTO_INCREMENT, USERID VARCHAR(255), PASSWORD VARCHAR(255), NAME VARCHAR(255))";
try (Statement stmt = con.createStatement()) {
stmt.execute(createTableSql);
System.out.println("Table 생성 완료");
}
//서버연결
HTTPServerLauncher httpServletLauncher = new HTTPServerLauncher("localhost", 8080);
HTTPRequestListener httpRequestListener = new HTTPRequestListener(con);
httpServletLauncher.registerHandler("/login", httpRequestListener);
httpServletLauncher.registerHandler("/signup", httpRequestListener);
httpServletLauncher.start();
} catch (IOException e) {
e.printStackTrace();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public class HTTPServerLauncher {
private HttpServer server = null;
public HTTPServerLauncher(String host, int port) throws IOException {
createServer(host, port);
}
private void createServer(String host, int port) throws IOException {
this.server = HttpServer.create(new InetSocketAddress(host, port), 0);
}
public void registerHandler(String path, HTTPRequestListener httpRequestListener) throws IOException {
this.server.createContext(path, httpRequestListener);
}
public void start(){
server.start();
}
public void stop(int delay){
server.stop(delay);
}
}
물론 서버를 여는 것까지는 라이브러리를 사용할 수 있었기 때문에 간편한 편이지만
package HTTP;
import DTO.HttpResponseDTO;
import DTO.LoginRequestDTO;
import DTO.SignUpRequestDTO;
import Service.LoginService;
import Service.Signup;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.util.stream.Collectors;
public class HTTPRequestListener implements HttpHandler {
HTTPResponseSender httpResponseSender = new HTTPResponseSender();
private final Connection connection;
public HTTPRequestListener(Connection connection) {
this.connection = connection;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
//로그인 기능
if(exchange.getRequestURI().toString().startsWith("/login")){
if (exchange.getRequestMethod().equals("POST")) {
LoginService loginService = new LoginService(connection);
String UserRequestString = HttpExchangeConverter(exchange);
String userId = UserRequestString.split("\\{")[1].split(":")[1].split(",")[0];
String password = UserRequestString.split("\\{")[1].split(":")[2].split(",")[0];
LoginRequestDTO loginRequestDTO = new LoginRequestDTO(userId, password);
ResponseEntity<HttpResponseDTO> response = loginService.login(loginRequestDTO);
httpResponseSender.sendResponse(exchange, response);
} else {
SendUnappropriateResponse(exchange);
}
}
//회원가입 기능
if(exchange.getRequestURI().toString().startsWith("/signup")){
if(exchange.getRequestMethod().equals("POST")) {
Signup signup = new Signup(connection);
String UserRequestString = HttpExchangeConverter(exchange);
String userId = UserRequestString.split("\\{")[1].split(":")[1].split(",")[0];
String password = UserRequestString.split("\\{")[1].split(":")[2].split(",")[0];
String name = UserRequestString.split("\\{")[1].split(":")[3].split(",")[0];
SignUpRequestDTO signUpRequestDTO = new SignUpRequestDTO(userId, password, name);
ResponseEntity<HttpResponseDTO> response = signup.signup(signUpRequestDTO);
httpResponseSender.sendResponse(exchange, response);
} else {
SendUnappropriateResponse(exchange);
}
}
}
private String HttpExchangeConverter(HttpExchange exchange) throws IOException {
InputStream is = exchange.getRequestBody();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
private void SendUnappropriateResponse(HttpExchange exchange) throws IOException {
ResponseEntity<HttpResponseDTO> response = new ResponseEntity<HttpResponseDTO>(405,
new HttpResponseDTO(405, "HTTP Method not supported"));
httpResponseSender.sendResponse(exchange, response);
}
}
모든 URL 요청에 대해서 하나하나 HTTP 메서드를 확인하고 관련된 서비스를 호출해야하는 것이 좀 번거로웠다.
즉,
- 서버 열기
- 서버에 요청받을 URL 등록하기
- 요청이 들어오면 이를 처리할 핸들러 클래스를 만들어서 관련된 서비스 호출하기
- DB 열고 초기 테이블 세팅하기
의 과정을 처리해야한다. 이 중에서 대부분의 기능들은 스프링에서 알아서 처리해주기 때문에 편리한 개발이 가능하다.

이 사진을 보면, 초록색 부분이 순수자바로 개발할 때 미리 사전에 세팅해야하지만, 결국 서비스에서 중요한 비즈니스 로직은 파란색 부분이다. 서버와 DB세팅에 시간을 할애하느라 비즈니스 개발할 시간이 줄어든다면? 이것을 자동으로 처리할 하나의 프레임워크가 필요해졌고 그래서 등장한 것이 스프링과 같은 백엔드 서버 프레임워크다.
2. 컨테이너
스프링의 꽃이라고 할 수 있겠다. 스프링 컨테이너를 통해 자동으로 객체를 주입받을 수 있다는 것인데, 앞서 보았듯이 DB 연결을하고 초기 세팅이 진행되는 곳은 main 함수다.
하지만 DB에 직접 접근해 데이터를 수정하는 곳은 MemberRepository와 같은 Repository 계층이다. 그러면 main에서 일어난 커넥션을 Repository 계층으로 옮겨주어야한다. 만약 스프링이 있다면 @Bean 어노테이션과 @Autowired 어노테이션을 통해 자동으로 주입받을 수 있겠지만..
Connection con = DriverManager.getConnection(URL, USER, PASSWORD);
...
HTTPRequestListener httpRequestListener = new HTTPRequestListener(con);
HTTPResponseSender httpResponseSender = new HTTPResponseSender();
private final Connection connection;
public HTTPRequestListener(Connection connection) {
this.connection = connection;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
//로그인 기능
if(exchange.getRequestURI().toString().startsWith("/login")){
if (exchange.getRequestMethod().equals("POST")) {
LoginService loginService = new LoginService(connection);
...
}
private final Connection connection;
private final MemberRepository memberRepository;
public LoginService(Connection connection) {
this.connection = connection;
memberRepository = new MemberRepository(connection);
}
public class MemberRepository {
private final Connection con;
public MemberRepository(Connection con) {
this.con = con;
}
이렇게 생성자를 통해서 MemberRepository까지 Connection을 넘겨주어야한다.
3. 여러 기능 구현
순수자바로 OAuth를 구현하라고하면... 솔직히 불가능하다.
https://dev-dx2d2y-log.tistory.com/5
[GDG] 홍대 맛집 아카이빙 프로젝트 백엔드 개발 #1 - OAuth에 대해서...
대학교 졸업할 때는 풀스택 개발자가 되기를 기원하며 백엔드 개발부터 시도했다. (사실 몇 년 전에 프론트엔드 맛보기 작업을 몇 개 했는데 내 스타일이 아니어서...) 백엔드 개발공부의 일환으
dev-dx2d2y-log.tistory.com
내가 블로그 거의 처음에 쓴 글인데.. 이 때는 지금보다 개발을 못할 때임에도 불구하고, OAuth의 개념을 제대로 알 지 못해도 스프링으로 OAuth 구현을 해냈다. 실제로 내가 순수 자바로 OAuth를 구현해내라고한다면 모든 OAuth의 개념을 제대로 이해한 뒤에 이리저리 찾아보면서 개발해야할 것이다.
뿐만 아니라 lombok, JPA, 그리고 OAuth를 진행할 때 반드시 사전설정해야하는 Security Config까지 다양한 기능들을 제공해서 순수자바로는 개발할 수 없는 기능들을 지원해준다.
솔직히 이정도 말고도 깊게 백엔드와 스프링에 파고든다면 더 많은 사실들을 알 수 있다. 그렇지만, 이렇게 간단한 프로젝트에서도 스프링을 왜 써야하는지, 그리고 어느 기능들을 제공하는지 알 수 있었다.
'CS > 백엔드' 카테고리의 다른 글
| [백엔드] Spring Security 첫 걸음 - DelegatingFilterProxy, FilterChainProxy로 Web Context Filter에 Spring Bean Filter 등록하기 (0) | 2026.02.14 |
|---|---|
| [백엔드] OAuth는 어떻게 진행되는가? (0) | 2026.02.06 |
| CSRF 공격이란 무엇인가? (0) | 2025.11.25 |
| AI를 이용할 때 할루시네이션 막아보기 (0) | 2025.11.21 |
| [CS] OPEN AI API로 간단한 메시지 보내고 받아보기 (0) | 2025.11.19 |
