【チームプロジェクトReview3】オブジェクト指向のための努力① サーバーアーキテクトト(PRGパターン、MVCパターンなど)
はじめに
今日は、プロジェクトのサーバーの設計とどのふうにオブジェクト指向のための努力をしたかについてお話したいと思います。
皆でAPIを設計した後、サーバー設計を担当する役割担当として、色々なことを工夫し、どのようにパッケージとMVCパターンを設計したかを中心にreviewします。
Restful API
API設計
Httpのメソッドを活用し、Restful APIを作成する努力をしました。我々はhttp formタグを利用し、GET,POST二つのメソッドを活用することにしました。
事前準備として、プロジェクトの前にAPIの作名方法、慣例、冪等性などを勉強し、チームプロジェクトに活用しました。
こちらの知識を活用し、我々は以下のルールでAPIを作成しました。
- 最初のURLは必ず複数形名詞に作名する(不可算名詞は除外)
例)/members/add , /items/outer , /notice/update- URLの中にURIを入れる際には二番目の「/」から作成する。今後、URIで認証機能を追加するためでもからだ。
例)/members /{memberNo} /update, /members /{memberNo}/orders、/items /{itemNo}/qna/{qnaNo}- なるべく名詞で作名することを意識する。
このようなルールを即して、作成したAPIとメソッド名になります。メッソド名の場合、GetはShow, GoToを使って統一性を与える努力をしました。
※User APIの詳細URL/Admin APIの詳細URL※
Javaのパッケージ構造もある程度考えつつ、Domainごとに異なる色を付けることで、各々が担当するパートをより分かりやすく区別するようにspreadsheetに記入しました。
PRGパターン
POSTの場合、冪等性がないので、注文・決済のようにDBのエンティティが追加される場合、重複決済、オーバーブッキングのような問題が起きる可能性があると考えました。
それもサービスにあって、大きな問題につながる可能性があるので、Postの後には必ずGetにRedirectするPRGパターンを活用しました。
SpringBootにはredirect:, @RedirectAttributeを通して、redirect時にもデーターを簡単に渡せるアノテーションがあったので、303のようなstatus code、httpServletResponseを使わずに簡単にデーターを保存することもできます。
/* 会員登録ページに移動(GET) */
@GetMapping("/add")
public String goToAddMemberPage(@ModelAttribute("memberAddDTO") MemberAddDTO memberAddDTO){
return "members/add-member";
}
/* 会員登録(POST) */
@PostMapping("/add")
public String addMember(@Validated(MemberValidationSequence.class) @ModelAttribute("memberAddDTO") MemberAddDTO memberAddDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
.
.
.
//成功時
try{
memberService.join(memberAddDTO);
redirectAttributes.addFlashAttribute("memberAddSuccess", "회원 가입이 완료되었습니다.");
return "redirect:/members/add-success";
}
.
.
.
/*会員登録後のRedirectページ*/
@GetMapping("/add-success")
public String goToAddMemberSuccess(RedirectAttributes redirectAttributes){
redirectAttributes.addFlashAttribute("memberAddSuccess", "회원 가입이 완료되었습니다.");
return "members/add-member-success";
}
MVCパータン
パッケージ設計
各最上位パッケージはDBのエンティティ に合わせて設計をしました。チーム員の役割が分かりやすいため、コミュニケーションを取りやすく、担当するパッケージのみ担当すれば良いため、Conflictが生じる可能性が低いと思ったからです。
設計したAPIを元に、SpringBootの @RequestMapping、@GetMapping、@PostMappingを使って一部のControllerをより二段階に分けました。
しかし、Memberの場合、会員ごとの注文情報、Q&A情報、Review情報もあったので、一つのDomainが大きくなる問題がありました。
そのような依存関係を解消するため、Order、Q&AなどのControllerにもMember関連情報をマッピングし、役割を分ける努力をしました。
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final MemberLoginService memberLoginService;
private final MemberInfoService memberInfoService;
.
.
.
/* User 会員の会員情報照会および変更 */
@GetMapping("/{memberNo}/update")
public String goToUpdateMemberPage(@PathVariable(name="memberNo") Long memberNo, Model model){
model.addAttribute("memberUpdateDTO",memberService.getMemberInfo(memberNo));
return "members/update-member";
}
@Controller
@RequiredArgsConstructor
public class OrderController {
private final ItemService_CMS itemService_cms;
private final ItemService itemService;
private final MemberOrderService memberOrderService;
private final NonMemberOrderService nonMemberOrderService;
private final CartService cartService;
.
.
.
/* User 会員の注文情報照会 */
@GetMapping("/members/{memberNo}/orders")
public String showMemberOrderList(@PathVariable(name="memberNo") Long memberNo,
@RequestParam(value="page", required=false, defaultValue="1") int page,
@ModelAttribute MemberOrderViewForm memberOrderViewForm,
HttpServletRequest request,Model model) {
.
.
.
return "orders/member-order-list";
}
レイヤードアーキテクチャ
ITスクールでは、MVC Model 2を学び、基本的なコーディングはこちらのパターンで行いましたが、時々何かのロジックが変更される際にコードをまるごど修正することが不便だと考えました。MVCパターンについて勉強した結果、ITスクールで勉強したModelはDBアクセスと核心ロジックを分離することで、依存度を減らすことが重要で、Modelをservice、Repositoryにまた分けることにしました。
Controllerは表現の領域、repositoryをDBアクセス領域に配置し、EntityとDTOにデーター関連オブジェクトを分け、ServiceからDTO内部の変換メソッドを呼び出すことにしました。こちらは会員情報アップデートロジックの一部です。
1.Interface
ユーザ(会員)が、WEBサイトからログインし、会員情報変更ページから修正したい情報を入力。
入力されたデーターはMyBatisとSpringbootによりDTO(MemberUpdateDTO)オブジェクトのフィールドに入ります。
.
.
.
<form th:action method="post" th:object="${memberUpdateDTO}">
<label class="label" for="memberName">
<h5 class="label__title">お名前*</h5>
<input id="memberName" name ="memberName" class="input input--member input--lg" type="text" th:field="*{memberName}" placeholder="韓国語(2~40자)" autocomplete="off"/>
<div class="label__error-container">
<h6 class="label__error" th:errors="*{memberName}">エラーメッセージ</h6>
</div>
</label>
<label class="label" for="memberHp">
<h5 class="label__title">携帯番号*</h5>
<input id="memberHp" name="memberHp" class="input input--member input--lg" type="text" th:field="*{memberHp}" placeholder="-を除外した数字(11字)" autocomplete="off"/>
<div class="label__error-container">
<h6 class="label__error" th:errors="*{memberName}">エラーメッセージ</h6>
</div>
</label>
.
.
.
<button type="submit" class="btn btn--white btn--lg btn--member-add-post">情報更新</button>
</form>
2. Controller
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final MemberLoginService memberLoginService;
private final MemberInfoService memberInfoService;
.
.
.
/* ユーザー 会員情報更新 */
@PostMapping("/{memberNo}/update")
public String updateMemberInfo(@Validated(MemberValidationSequence.class) @ModelAttribute MemberUpdateDTO memberUpdateDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
.
.
.
try{
memberService.update(memberUpdateDTO);
redirectAttributes.addFlashAttribute("memberUpdateSuccess", "会員情報が更新されました。");
return "redirect:/members/{memberNo}/update";
.
.
.
}
3. Service(DTO->Entity)
ControllerからのDTOオブジェクトをEntityオブジェクトに変換した後、Repositoryに転送し、会員情報更新が成功か失敗かのみを判断します。失敗した場合、例外を発生させます。
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
.
.
.
@Transactional
public void update(MemberUpdateDTO memberUpdateDTO){
Member member = MemberUpdateDTO.MemberUpdateDTOToMember(memberUpdateDTO);
memberRepository.findByHp(member.getMemberHp())
.ifPresent(existingMember -> {
//変更したい携帯番号がDB上にあれば例外処理(自分のDB上の番号は除外)
if (!existingMember.getMemberId().equals(member.getMemberId())) {
throw new DuplicatedFieldException("memberHp", "存在している携帯番号です。");
}
});
memberRepository.update(member);
}
3-1 DTO(MemberUpdateDTO)
.
.
.
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class MemberUpdateDTO {
private Long memberNo;
private String memberEmail;
private String memberId;
.
.
.
@NotBlank(message = "携帯番号をご入力ください。", groups = MemberNotBlankGroup.class)
@Pattern(regexp = "^01(?:0|1|[6-9])\\d{7,8}$", message = "正しい携帯番号形式ではありません。", groups = MemberPatternGroup.class)
private String memberHp;
.
.
.
public static Member MemberUpdateDTOToMember(MemberUpdateDTO memberUpdateDTO){
Member member = new Member();
member.setMemberId(memberUpdateDTO.getMemberId());
member.setMemberPw(memberUpdateDTO.getMemberPw());
member.setMemberHp(memberUpdateDTO.getMemberHp());
member.setMemberName(memberUpdateDTO.getMemberName());
member.setMemberPostalCode(memberUpdateDTO.getMemberPostalCode());
member.setMemberAddressBasic(memberUpdateDTO.getMemberAddressBasic());
member.setMemberAddressDetail(memberUpdateDTO.getMemberAddressDetail());
return member;
}
.
.
.
}
3-2 Entity(Member)
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
private Long memberNo;
private String memberId;
private String memberHp;
private String memberEmail;
private String memberPw;
private String memberName;
private Integer memberPostalCode;
private String memberAddressBasic;
private String memberAddressDetail;
4. Repository
SQLの転送を担当するMemberMapper、Membermapper.xmlを依存します。結果をServiceにリターンします。DBアクセスのみ担当します。
.
.
.
@Repository
@RequiredArgsConstructor
public class MyBatisMemberRepository implements MemberRepository {
private final MemberMapper memberMapper;
@Override
public void update(Member member) {
memberMapper.update(member);
}
.
.
.
@Override
public Optional<Member> findByHp(String memberHp) {
return Optional.ofNullable(memberMapper.findByHp(memberHp));
}
.
.
.
}
Discussion