길고 긴 실습은 거의 끝이 났다
이번엔 간단한 웹 어플리케이션의 구조에 대해 정리해보려고 한다!
간단한 웹 어플리케이션의 구성 요소
간단한 웹 어플리케이션을 개발할 때 사용하는 전형적인 구조는 다음 요소를 포함한다.
서비스의 구현
서비스의 구현에 대해 비밀번호 변경 예시를 들어 내용을 자세하게 풀어보면
비밀번호 변경 기능을 위한 로직에는
이렇게 있다.
웹 어플리케이션을 사용하든 명령행에서 실행하든 비밀번호 변경 기능을 제공하는 서비스는 동일한 로직을 수행한다. 이런 로직들은 한 번의 과정이 아닌 몇 단계의 과정을 거치며 중간 과정에서 실패가 나면 이전까지 했던 것을 취소하고 모든 과정을 성공적으로 진행했을 때 완료해야 한다.
이런 이유로 서비스 메서드를 트랜잭션 범위에서 실행한다.
package spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
public class ChangePasswordService {
@Autowired
private MemberDao memberDao;
@Transactional
//(value = "test") 이 속성 없으면 PlatformTransactionManager 타입으로 찾아서 사용
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);
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
서비스를 구현할 때 한 서비스 클래스에 제공할 기능의 개수는 몇개가 적당할까 ?! 답은 따로 없지만 상황이나 실무에 맞게 같은 데이터를 사용하는 기능들을 한 개의 서비스 클래스에 모아 구현하거나 기능별로 서비스 클래스를 작성하면 된다.
🙋♀️ 기능별 서비스 클래스를 작성하면 장점이 뭔가요?
💡 한 클래스의 코드 길이를 일정 수준 안에서 유지할 수 있어서 기존 코드를 수정하거나 기능 확장이 용이하다.
서비스 메서드는 기능 실행 후 크게 두 가지 방식으로 결과를 알려준다.
package spring;
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());
}
}
이 서비스 클래스를 호출한 클래스에서 결과에 따른 분기처리가 이루어진다.
컨트롤러에서의 DAO 접근
서비스 메서드에서 어떤 로직도 수행하지 않고 단순히 DAO의 메서드만 호출하고 끝나는 코드도 있다.
예를 들어 회원 데이터 조회를 위한 서비스 메서드를 다음과 같이 구현하곤 한다.
public class MemberService {
public Member getMember(long id) {
return memberDao.selectById(id);
}
}
이 서비스 메서드를 이용해 컨트롤러에서 회원 정보를 구한다.
@GetMapping("/members/{id}")
public String detail(@PathVariable("id") Long memId, Model model) {
Member member = memberService.getMemberId(memId);
if (member == null) {
throw new MemberNotFoundException();
}
model.addAttribute("member", member);
return "member/memberDetail";
}
이렇게 되면 사실상 memberDao.selectById() 메서드를 실행하는 것과 동일하다. 이 경우 컨트롤러는 서비스를 사용해야 한다는 압박에서 벗어나 직접 DAO에 접근해도 큰 틀에서 웹 어플리케이션의 계층 구조는 유지된다고 본다.
@GetMapping("/members/{id}")
public String detail(@PathVariable("id") Long memId, Model model) {
Member member = memberDao.selectById(memId);
if (member == null) {
throw new MemberNotFoundException();
}
model.addAttribute("member", member);
return "member/memberDetail";
}
‼️ 컨트롤러에서 서비스 계층을 거치지 않고 바로 데이터 접근 계층의 DAO를 사용하는 방식은 개발자마다 호불호가 갈린다. 어떤 방식이 좋다는 식의 정답은 없으니 서비스의 역할과 DAO의 역할을 정의해나가며 선호하는 방식을 정립해나가면 된다 ‼️
패키지 구성
웹 요청을 처리하기 위한 영역에는 컨트롤러 클래스와 관련 클래스들이 위치한다. 커맨드 객체의 값을 검증하기 위한 Validator도 웹 요청 처리 영역에 위치할 수 있는데 관점에 따라 Validator를 기능 제공 영역에 위치시킬 수도 있다.
기능 제공 영역에는 기능 제공을 위해 필요한 서비스, DAO, 그리고 Member와 같은 모델 클래스가 위치한다. 기능 제공 영역은 다시 service, dao, model과 같은 세부 패키지로 구분하기도 한다.