이 포스팅은 웹 어플리케이션의 구성요소와 서비스 구현, 패키지 구성에 대해서 다룹니다.
웹 어플리케이션을 개발할 때는 다음과 같은 요소를 포함합니다.
- 프론트 서블릿
- 컨트롤러 + 뷰
- 서비스
- DAO
DispatcherServlet -> 컨트롤러 -> 서비스 -> DAO
- 프론트 서블릿
웹 브라우저의 모든 요청을 받는 창구 역할. 요청을 분석해서 알맞은 컨트롤러에 전달.
스프링 MVC에서는 DispatcherServlet이 프론트 서블릿의 역할을 수행.
- 컨트롤러
실제 웹 브라우저의 요청을 처리. 클라이언트의 요청을 처리하기 위해 알맞은 기능을 실행하고 그 결과를 뷰에 전달.
어플리케이션이 제공하는 기능과 사용자 요청을 연결하는 매개체로 비즈니스 로직을 직접 수행하지는 않음.
- 클라이언트가 요구한 기능 실행
- 응답 결과를 생성하는데 필요한 모델 생성
- 응답 결과를 생성할 뷰 선택
- 서비스
기능의 로직을 구현. 비밀번호를 변경하는 기능을 제공하려면 수정 폼을 제공하고, 로그인 여부를 확인하고, 실제로 비밀번호를 변경해야 함.
이를 실제로 구현하는 것을 서비스에서 함. DB 연동 필요시 DAO를 사용.
- DAO (Data Access Object)
DB와 웹 어플리케이션 간에 데이터를 이동시켜 주는 역할. 어플리케이션은 DAO를 통해서 DB와 어플리케이션 간에 데이터를 이동시켜 주는 역할. 어플리케이션은 DAO를 통해서 DB에 데이터를 추가하거나 DB에서 데이터를 읽음. 목록이나 상세 화면과 같이 데이터를 조회하는 기능만 있고 부가적인 로직이 없는 경우에는 컨트롤러에서 직접 DAO를 사용.
구성 요소에 대해서 간단하게 설명해봤습니다. 그럼 이제 이 중에서 서비스의 구현에 대해서 더 자세히 얘기해 보겠습니다.
만약 비밀번호를 변경하는 기능을 구현하다고 가정해 보겠습니다. 그럼 서비스에서는 어떤 일이 일어날까요?
- DB에서 비밀번호를 변경할 회원의 데이터를 구함
- 존재하지 않으면 익셉션 발생
- 회원 데이터의 비밀번호를 변경
- 변경 내역을 DB에 저장
비밀번호를 변경하는 과정은 단순히 비밀번호만 바꾸는 것이 아니라 이렇게 여러 단계를 거쳐서 비밀번호를 변경하게 됩니다.
만약 여러 단계를 거치다가 앞의 과정은 성공했으나 중간과정이 실패하면 어떻게 될까요? 데이터의 원자성이 깨지게 되겠죠?
이를 방지하기 위해서 우리는 기능을 트랜잭션 범위 내에서 실행해야 합니다.
이를 스프링의 @Transactional 애노테이션을 이용하면 트랜잭션 단위로 기능을 수행할 수 있습니다.
@Transactional
public void changePassword(String email, String oldPwd, String newPwd) {
Member member = memberDao.selectByEmail(email);
if (member == null)
throw new MemberNotFoundException();
member.changePassword(oldPwd, newPwd);
memberDao.update(member);
}
위의 코드는 그 예시입니다. 비밀번호를 바꾸는 로직을 수행하는 메서드에 @Transactional을 붙이면 메서드 단위로 트랜잭션을 관리하게 됩니다.
이외에도 같은 데이터를 사용하는 기능들을 한 개의 서비스 클래스에 모아서 구현할 수도 있습니다.
예를 들어 회원가입 기능과 비밀번호 변경 기능은 모두 회원에 대한 기능이므로 다음과 같이 MemberService라는 클래스를 만들어서 회원과 관련된 기능을 제공하도록 구현할 수도 있을 것입니다.
서비스에서 사용하는 메서드는 위의 예시처럼 파라미터로 기본 자료형 외에도 커맨드 객체를 사용해서 타입을 전달할 수 있어서 편리합니다. 또한 커맨드 객체로 받고 그 객체의 프로퍼티를 서비스 메서드의 인자로 전달할 수도 있습니다.
@GetMapping
public String form(@ModelAttribute("command") ChangePwdCommand pwdCmd) {
return "edit/changePwdForm";
}
이처럼 커맨드 클래스를 작성한 이유는 스프링 mvc가 제공하는 폼 값 바인딩과 검증, 스프링 폼 태그와의 연동기능을 사용하기 위해서입니다.
서비스 메서드는 기능을 실행한 후에 결과를 알려주어야 합니다. 결과는 크게 두 가지 방식으로 알려줍니다.
리턴값을 이용한 정상 결과, 그리고 익셉션을 이용한 비정상 결과입니다.
AuthService.java
public class AuthService {
private MemberDao memberDao;
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
public AuthInfo authenticate(String email, String password) {
Member member = memberDao.selectByEmail(email);
if (member == null) {
throw new WrongIdPasswordException();
}
if (!member.matchPassword(password)) {
throw new WrongIdPasswordException();
}
return new AuthInfo(member.getId(),
member.getEmail(),
member.getName());
}
}
authenticate 메서드는 리턴타입을 AuthInfo로 사용하고 있습니다. 인증에 성공하는 경우에는 AuthInfo 객체를, 실패할 경우에는 익셉션을 던집니다.
반면 서비스에서 어떤 로지도 수행하지 않고 단순히 DAO의 메서드만 호출하고 끝나는 코드도 있습니다.
예를 들어 회원 데이트를 조회하는 경우가 그렇습니다. 쿼리를 조회할 뿐 서비스 로직에서 딱히 해주는 것은 없습니다.
이는 사실상 DAO를 직접 호출하는 것과 동일합니다. 이런 경우에는 서비스 로직을 거치지 않고 컨트롤러에서 바로 DAO에 접근해도 어플리케이션의 계층 구조는 유지된다고 봅니다.
그럼 이렇게 웹 어플리케이션에서 자주 사용되는 구조에 대해서 살펴봤습니다.
그렇다면 각 구성 요소의 패키지는 어떻게 구분해 줘야 할까요?
웹 요청을 처리하기 위한 영역과 회원 기능을 제공하기 위한 영역으로 구분할 수 있습니다.
웹 요청 처리 영역에는 Controller와 커맨드 객체의 값을 검증하기 위한 Validator도 이 영역에 해당하지만, 따로 Validator를 구분하기도 합니다. 이와 같은 웹 영역의 패키지는 web, memeber와 같이 영역에 알맞은 패키지 이름을 사용하게 됩니다.
기능 제공 영역에는 기능 제공을 위해 필요한 서비스, DAO, 모델 클래스가 위치합니다.
위의 이미지처럼 패키지를 나누곤 하는데 사실 패키지를 나누는 방법에 정답은 없습니다.
하지만 이처럼 패키지를 구분하면 코드를 체계적으로 관리할 수 있습니다.
출처: 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문
'Spring' 카테고리의 다른 글
[Spring] 프로필과 프로퍼티 파일 (1) | 2023.12.31 |
---|---|
[Spring] JSON 응답과 요청 처리(@RestController, @RequestBody, ResponseEntity) (0) | 2023.12.31 |
[Spring] 스프링 MVC 4 : 날짜 값 변환, @PathVariable, 익셉션 처리 (1) | 2023.12.24 |
[Spring] 스프링 MVC 3 : 세션, 인터셉터, 쿠키 (Session, Interceptor, Cookie) (1) | 2023.12.16 |
[Spring] 스프링 MVC 2 : 메시지, 커맨드 객체 검증 (1) | 2023.12.08 |