1차 프로젝트는 리액트로 진행했었는데 2차 프로젝트는 JSTL, EL을 사용해서 JSP 페이지로 한다고 해서 그렇게 진행하고 있었다.
JSP 페이지는 스크립트릿 (Scriptlet): <% %>을 사용할 수 있다고 해서
JSP 페이지 안에 자바 코드를 계속 쓰면서 프로젝트를 진행하고 있었다.
그런데... 쌤이 MVC 수업을 하시다가 갑자기 이번 프로젝트는 MVC 패턴을 사용해서 진행해야 한다고 하셔서 머리가 하얘졌다..
MVC 패턴이 뭐지? 부터 시작해서 그럼 자바로 만든 걸 다 바꿔야 하는 건가? 나름 좀 많이 했는데.. 하면서 걱정이 되기 시작했다!
하지만 뭐.. 하라면 하면 되고 바꿔야 한다면 바꾸면 되고 공부도 할 겸 한번 해보자! 하고 바로 시작했다!
일단 MVC패턴 개념부터 잡기 위해 다시 정리하면서 공부해 보려고 한다!
MVC가 뭐지?
수업내용과 검색해서 정리해 본 결과 Model View Controller 패턴이라고 한다.
그럼 왜 MVC 패턴을 쓰지? 궁금했는데
소프트웨어를 Model, View, Controller 라는 3가지 구성으로 구분한 개발 방법론이라는 걸 알았다.
(코드를 이런 식으로 짜자! 약속! 이렇게 이해했다.)
사용자가 컨트롤러를 사용하면
컨트롤러는 모델에서 데이터를 받아오고
받아온 데이터를 뷰에서 보여주면서 사용자에게 다시 전달하는 구조다.
그다음으로는 컨트롤러, 모델, 뷰에 대해서 이해해 보려고 했다.
컨트롤러(Controller)
사용자가 브라우저에서 URL을 통해 요청을 보내면 제일 먼저 만나는게 컨트롤러다.
사용자가 댓글을 작성하고 버튼을 눌러서 작성 요청을 보내면 → comment/insert로 URL 요청을 보낸다.
// 댓글 작성
@PostMapping("/insert")
public String insertTalk(@RequestParam("talkText") String talkText, HttpSession session, RedirectAttributes redirectAttributes) {
// TalkVO 객체 생성
TalkVO talkVO = new TalkVO(); // 사용자가 입력한 댓글 데이터를 담을 객체 생성
talkVO.setTalkText(talkText); // 사용자가 입력한 댓글 내용(talkText)을 TalkVO 객체에 저장
// 세션에서 로그인할 때 저장된 member 객체 가져오기
// 세션에 로그인한 사용자의 정보(M_MemberVO)를 저장해 두었으므로 그 정보를 꺼내옴
M_MemberVO member = (M_MemberVO) session.getAttribute("member");
// TalkVO 객체에 세션에서 가져온 로그인한 사용자의 정보를 저장
// 로그인한 사용자의 닉네임, 이메일, 프로필 이미지를 TalkVO에 저장
talkVO.setTalkNickname(member.getM_nickname()); // 로그인한 사용자의 닉네임 설정
talkVO.setTalkEmail(member.getM_email()); // 로그인한 사용자의 이메일 설정
talkVO.setTalkProfile(member.getM_profile()); // 로그인한 사용자의 프로필 이미지 설정
// 댓글 데이터를 DB에 삽입하는 서비스 호출
// TalkService의 insertTalk() 메서드를 호출하여 TalkVO 객체를 전달, 댓글 데이터를 DB에 삽입
int result = talkService.insertTalk(talkVO);
// 댓글 등록 성공 여부에 따라 메시지를 설정하고 리다이렉트할 때 전달
if (result > 0) { // 댓글 등록 성공 시
redirectAttributes.addFlashAttribute("message", "댓글이 성공적으로 등록되었습니다."); // 성공 메시지 추가
} else { // 댓글 등록 실패 시
redirectAttributes.addFlashAttribute("message", "댓글 등록에 실패했습니다. 다시 시도해주세요."); // 실패 메시지 추가
}
// 댓글 리스트 페이지로 리다이렉트
return "redirect:/HotPlace/hotplace2"; // hotplace2.jsp로 리다이렉트
}
컨트롤러 자체는 비즈니스 로직을 처리하지 않고 로직을 처리할 친구한테 던져준다! 예를 들어
컨트롤러는 사용자가 적은 댓글을 담아서 Service 친구한테 비즈니스 로직을 처리하라고 요청한다.
int result = talkService.insertTalk(talkVO);
근데 여기서 또 한 번 생각해봤다. 항상 Service랑 ServiceImpl이랑 같이 쓰여져서 이것도 제대로 알고 싶었다.
서비스(Service, ServiceImpl)
서비스는 비즈니스 로직을 정의하는 인터페이스다.
여기서 인터페이스는 어떤 기능을 제공할지 메서드의 형태! 만 정의한다.
public interface TalkService {
int insertTalk(TalkVO talkVO); // 댓글 삽입
}
그 후 구체적인 기능을 구현한 구현체인 ServiceImpl로 넘어간다.
@Service
public class TalkServiceImpl implements TalkService {
@Autowired
private TalkDAO talkDAO;
// 댓글 삽입
@Override
public int insertTalk(TalkVO talkVO) {
return talkDAO.insertTalk(talkVO);
}
}
여기서 insertTalk(TalkVO talkVO)로 TalkVO 객체를 받아서 댓글 등록 로직을 처리한다.
talkVO로 컨트롤러에서 넘어온 댓글 데이터들을 담았다.
TalkVO는 댓글 데이터를 담고 있는 객체고 이렇게 구성했다.
@Data
public class TalkVO {
private int talkIdx; // 회원번호
private String talkNickname; // 닉네임
private String talkEmail; // 회원ID
private String talkText; // 본문 내용
private String talkProfile; // 프로필 이미지 URL
private Date talkCreatedAt; // 작성일자
private Date talkUpdatedAt; // 수정일자
}
롬복을 사용했고 구성해야하는 페이지에 필요한 값들을 담아줬다.
다시 Impl 로 돌아가서
talkDAO.insertTalk(talkVO);
이 부분에서 DAO를 호출해서 실제로 DB에 댓글을 삽입하는 역할을 하게 했다.
구현체가 Impl이지만 또 구체적인 동작은 DAO로 넘기는 이유가 궁금했다.
Service는 비즈니스 로직을 처리한다. 주로 비즈니스 요구사항을 처리하는데 집중한다고 했다.
ex) 댓글 작성에 대한 로직은 서비스 계층에서 정의하고,
이 과정에서 어떤 추가 작업(데이터 검증, 트랜잭션 관리 )이 필요 한지를 처리한다.
그리고 데이터베이스와의 직접 상호작용은 서비스 계층의 책임이 아니고 DAO에서 담당해야 한다고 했다.
그래서 구현체인 Impl은 비즈니스 로직만 담당하고
DB와의 직접 상호작용은 DAO의 책임이니 DAO한테 맡겨야 한다고 했다.
다오(DAO)
데이터베이스와 직접 상호작용을 위해 DAO를 사용한다고 한다.
서비스에서 직접 SQL을 실행하지 않는 이유는
데이터베이스 접근 로직을 분리해서 코드를 깔끔하게 유지하고 쉽게 테스트하기 위함이라고 한다.
(만약 후에 데이터베이스 구조가 변경되거나 새로운 데이터베이스를 사용하게 된다면 DAO만 수정하면 되니까!)
@Repository
public class TalkDAO {
// 댓글 입력
public int insertTalk(TalkVO vo) {
return sqlSession.insert(MAPPER + ".insertTalk", vo);
}
}
sqlSession.insert() 라는 MyBatis에서 제공하는 메서드로 DB에 insert 쿼리 작업을 시키고
MAPPER + ".insertTalk"라는 MyBatis의 MAPPER에 정의된 SQL 쿼리랑 연결되게 하고
TalkVO 객체에 담긴 댓글 데이터를 전달시켰다!
또 여기서 물음표
DAO에서 DB와의 직접 상호작용을 한다고 했는데 왜 또 MAPPER로 보내는 건지 궁금했다.
매퍼(MAPPER)
Mapper는 SQL 쿼리를 XML 파일로 관리하면서 DAO와 데이터베이스의 중간 역할을 한다고 했다.
DAO에서 직접 SQL 쿼리를 작성하지 않고 Mapper한테 넘기면서 Mapper가 SQL 쿼리를 관리하게 했다.
이렇게 하면 SQL 쿼리와 비즈니스 로직의 분리를 통해 코드의 가독성을 높이고
나중에 SQL을 수정할 때 쉽게 수정할 수 있으며, MAPPER만 수정하면 되니까 유지 보수성이 높아진다고 한다.
또한 여러 DAO에서 동일한 SQL 쿼리를 쓸 수 있으니 재사용성도 높아진다고 한다.
(아직 많은 코드들을 다뤄본 게 아니라 확 체감이 들진 않았다..)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.human.web.mapper.TalkMapper">
<!-- 댓글 삽입 -->
<insert id="insertTalk" parameterType="com.human.web.vo.TalkVO">
INSERT INTO talk (talk_nickname, talk_email, talk_text, talk_profile)
VALUES (#{talkNickname}, #{talkEmail}, #{talkText}, #{talkProfile})
</insert>
</mapper>
id=insertTalk 라는 id 값으로 DAO나 service에서 이 ID 값으로 쿼리를 호출한다.
(ex_ DAO에서 talkDAO.insertTalk()를 호출하면 MAPPER에 있는 id=insertTalk 쿼리가 실행된다.)
talk 테이블의 열에 맞게 구성했고 쿼리문을 써줬다.
INSERT INTO talk (talk_nickname, talk_email, talk_text ... )
VALUES (#{talkNickname}, ... )
이렇게 해서 MVC 패턴대로 댓글 입력 수정 완료!!
전체적인 흐름
JSP → Controller → Service + ServiceImpl → DAO → Mapper → JSP
MVC에 해당하는 부분
Model : Service + ServiceImpl , DAO , Mapper
View : JSP
Controller : Controller
이렇게 MVC 패턴에 대해 찾아보고 공부하면서 조금 이해할 수 있게 되었다!
마지막으로 기존 JSP로 작성한 댓글 작성 로직!
이땐 javascript, DAO, DTO, DBCP 이렇게 4개로 만들었다.
javascript
// 댓글 작성하기
document.addEventListener("DOMContentLoaded", function() {
const submitButton = document.getElementById('submitButton');
if (submitButton) {
submitButton.addEventListener('click', function() {
const commentText = document.getElementById('commentText');
if (commentText) {
const commentValue = commentText.value.trim();
if (commentValue) {
// 서버로 데이터 전송 (POST 방식)
fetch('submitTalk.jsp', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'talkText=' + encodeURIComponent(commentValue) // talkText로 수정
})
.then(response => response.text())
.then(result => {
if (result.trim() === 'success') {
alert('댓글이 성공적으로 등록되었습니다.');
window.location.reload(); // 페이지 새로고침으로 댓글 갱신
} else {
alert('댓글 등록에 실패했습니다.');
window.location.reload(); // 페이지 새로고침으로 댓글 갱신
}
})
.catch(error => {
console.error('댓글 등록 중 오류:', error);
});
} else {
alert('댓글을 입력해주세요.');
}
}
});
}
});
SubmitTalk.jsp
<%
request.setCharacterEncoding("UTF-8");
// 댓글 내용 가져오기
String talkText = request.getParameter("talkText"); // talkText로 수정
String talkNickname = (String) session.getAttribute("memberNickname"); // 세션에서 닉네임 가져오기
String talkEmail = (String) session.getAttribute("memberEmail"); // 세션에서 이메일 가져오기
// TalkDTO 객체 생성
TalkDTO talk = new TalkDTO();
talk.setTalkNickname(talkNickname); // 세션에서 가져온 닉네임
talk.setTalkEmail(talkEmail); // 세션에서 가져온 이메일
talk.setTalkText(talkText); // 입력된 댓글 내용
// TalkDAO 객체 생성 및 댓글 저장
TalkDAO talkDAO = new TalkDAO();
int result = talkDAO.insertTalk(talk);
if (result > 0) {
out.print("success");
} else {
out.print("failure");
}
%>
TalkDAO
package dao;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import dto.TalkDTO;
import jdbc.DBCP;
public class TalkDAO extends DBCP {
// 댓글 입력하기
public int insertTalk(TalkDTO dto) {
int result = 0;
String sql = "INSERT INTO talk (talk_nickname, talk_email, talk_text) VALUES (?, ?, ?)";
try {
conn.setAutoCommit(false); // 오토 커밋 중지
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, dto.getTalkNickname()); // talk_nickname 설정
pstmt.setString(2, dto.getTalkEmail()); // talk_email 설정
pstmt.setString(3, dto.getTalkText()); // talk_text 설정
result = pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
System.out.println("댓글 입력 중 예외 발생");
e.printStackTrace();
try {
conn.rollback(); // 댓글 입력 중 예외 발생 시 롤백
} catch (SQLException e1) {
System.out.println("롤백 실패");
e1.printStackTrace();
}
} finally {
try {
conn.setAutoCommit(true); // 오토 커밋 가능
} catch (SQLException e) {
e.printStackTrace();
}
close();
}
return result;
}
TalkDTO
package dto;
import java.util.Date;
import lombok.Data;
@Data
public class TalkDTO {
private int talkIdx; // 회원번호
private String talkNickname; // 닉네임
private String talkEmail; // 회원ID
private String talkText; // 본문 내용
private Date talkCreatedAt; // 작성일자
private Date talkUpdatedAt; // 수정일자
}
댓글 등록 뿐만 아니라
수정, 삭제, 공공데이터API 부분도 전부 MVC 패턴으로 바꿔놨지만
한번 더 이해하기 위해서 정리!