지난 2편에서는 Assembler와 이를 사용하는 Main 클래스를 작성해봤다.
이번에는 객체 조립에 Assembler 대신 스프링을 사용하도록 하겠다.📌 순서
- 스프링 설정 정보 작성하기
- MainForSpring 클래스 작성하기
- DI 방식 살펴보기
- 프로젝트 정리하기
스프링을 사용하기 위해 어떤 객체를 생성하고, 의존을 어떻게 주입할지 정의한 설정 정보를 작성해야 한다.
설정 코드는 config 패키지에 생성하도록 한다.
AppXtx.java✏️
package config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import spring.ChangePasswordService; import spring.MemberDao; import spring.MemberRegisterService; @Configuration public class AppCtx { @Bean public MemberDao memberDao() { return new MemberDao(); } @Bean public MemberRegisterService memberRegSvc() { return new MemberRegisterService(memberDao()); } @Bean public ChangePasswordService changePwdSvc() { ChangePasswordService pwdSvc = new ChangePasswordService(); pwdSvc.setMemberDao(memberDao()); return pwdSvc; } }
@Configuration 애노테이션은 스프링 설정 클래스를 의미한다.
@Bean 애노테이션은 해당 메서드가 생성한 객체를 스프링 빈이라고 설정한다. 이 때 메서드의 이름을 빈 객체의 이름으로 사용한다.생성자 주입✔️
@Bean public MemberRegisterService memberRegSvc() { return new MemberRegisterService(memberDao()); }
위 코드에서 MemberRegisterService 생성자를 호출하는 부분을 보면, memberDao()가 생성한 객체를 생성자를 통해 주입하는 것을 확인할 수 있다.
세터 주입✔️
@Bean public ChangePasswordService changePwdSvc() { ChangePasswordService pwdSvc = new ChangePasswordService(); pwdSvc.setMemberDao(memberDao()); return pwdSvc; }
위 코드에서는 setMemberDao() 메서드를 통해 MemberDao 객체를 주입하고 있다. 이를 세터를 이용한 DI라고 한다.
지금껏 작성한 설정 클래스를 이용해 스프링 컨테이너를 생성해야 한다.
스프링 컨테이너를 사용하여 빈 객체를 구하고 사용할 수 있다.
MainForSpring 클래스를 작성하자.MainForSpring.java✏️
package main; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import config.AppCtx; import spring.ChangePasswordService; import spring.DuplicateMemberException; import spring.MemberNotFoundException; import spring.MemberRegisterService; import spring.RegisterRequest; import spring.WrongIdPasswordException; public class MainForSpring { private static ApplicationContext ctx = null; public static void main(String[] args) throws IOException { ctx = new AnnotationConfigApplicationContext(AppCtx.class); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); while (true) { System.out.println("명령어를 입력하세요: "); String command = reader.readLine(); if (command.equalsIgnoreCase("exit")) { System.out.print("종료합니다."); break; } if (command.startsWith("new ")) { processNewCommand(command.split(" ")); continue; } else if (command.startsWith("change ")) { processChangeCommand(command.split(" ")); continue; } printHelp(); } } private static void processNewCommand(String[] arg) { if (arg.length != 5) { printHelp(); return; } MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class); RegisterRequest req = new RegisterRequest(); req.setEmail(arg[1]); req.setName(arg[2]); req.setPassword(arg[3]); req.setConfirmPassword(arg[4]); if (!req.isPasswordEqualToConfirmPassword()) { System.out.println("암호와 확인이 일치하지 않습니다.\n"); return; } try { regSvc.regist(req); System.out.println("등록했습니다.\n"); } catch (DuplicateMemberException e) { System.out.println("이미 존재하는 이메일입니다.\n"); } } private static void processChangeCommand(String[] arg) { if (arg.length != 4) { printHelp(); return; } ChangePasswordService changePwdSvc = ctx.getBean("changePwdSvc", ChangePasswordService.class); try { changePwdSvc.changePassword(arg[1], arg[2], arg[3]); System.out.println("암호를 변경했습니다.\n"); } catch (MemberNotFoundException e) { System.out.println("존재하지 않는 이메일입니다.\n"); } catch (WrongIdPasswordException e) { System.out.println("이메일과 암호가 일치하지 않습니다.\n"); } } private static void printHelp() { System.out.println(); System.out.println("잘못된 명령입니다. 아래 명령어 사용법을 확인하세요."); System.out.println("명령어 사용법"); System.out.println("new 이메일 이름 암호 암호확인"); System.out.println("change 이메일 현재비번 변경비번"); System.out.println(); } }
2편에서 작성했던 MainForAssembler 클래스와 거의 같은 코드다. 다른 점은 Assembler 클래스 대신 스프링 컨테이너(ApplicationContext)를 사용한 점이다.
달라진 코드를 위주로 살펴보자.ctx 초기화✔️
private static ApplicationContext ctx = null;
ApplicationContext를 사용하기 위해
ctx
를 초기화한 부분이다.ctx = new AnnotationConfigApplicationContext(AppCtx.class);
ctx
객체에 스프링 설정 클래스인 AppCtx를 사용해 생성한 스프링 컨테이너를 담는다.getBean()✔️
MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);
ChangePasswordService changePwdSvc = ctx.getBean("changePwdSvc", ChangePasswordService.class);
이 두 코드는 Assembler 대신
스프링 컨테이너
의 getBean() 메서드를 통해 빈 객체를 가져오고 있다.
앞서 스프링 설정 클래스를 작성하며 설명했듯 의존 주입에는 생성자를 이용한 방식과 세터 메서드를 이용한 방식이 있다. 간단한 기능을 추가하며 더 살펴보도록 하자.
생성자 방식을 이용한 의존 주입을 살펴보기 위해 회원 정보를 출력하는 기능을 추가할 것이다.
MemberDao.java 에 코드 추가✏️
public Collection<Member> selectAll() { return map.values(); }
담아둔 회원 정보를 반환하는 selectAll() 메서드를 MemberDao 클래스에 추가한다.
MemberPrinter.java✏️
package spring; public class MemberPrinter { public void print(Member member) { System.out.printf("회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%tF\n", member.getId(), member.getEmail(), member.getName(), member.getRegisterDateTime()); } }
파라미터로 받은 회원 정보를 출력하는 print() 메서드를 담은 클래스다.
MemberListPrinter.java✏️
모든 회원들의 정보를 출력하기 위해 필요한 클래스를 작성한다.
package spring; import java.util.Collection; public class MemberListPrinter { private MemberDao memberDao; private MemberPrinter printer; public MemberListPrinter(MemberDao memberDao, MemberPrinter printer) { this.memberDao = memberDao; this.printer = printer; } public void printAll() { Collection<Member> members = memberDao.selectAll(); members.forEach(m -> printer.print(m)); } }
이 클래스는 생성자의 파라미터로 두 객체를 전달받는다.
printAll() 메서드는 memberDao의 selectAll() 메서드를 통해 모든 회원들의 정보를 불러와서 전달받은 MemberPrinter 객체를 통해 하나씩 print() 메서드로 출력한다.AppCtx.java 에 설정 추가✏️
이제 AppCtx 클래스에도 두 인자를 받는 생성자를 사용하는 설정을 추가한다.
@Bean public MemberPrinter memberPrinter() { return new MemberPrinter(); } @Bean public MemberListPrinter listPrinter() { return new MemberListPrinter(memberDao(), memberPrinter()); }
MainForSpring.java 에 코드 추가✏️
마지막으로 MainForSpring 클래스에 이 기능을 사용하기 위한 코드를 추가하자.
while (true) { System.out.println("명령어를 입력하세요: "); String command = reader.readLine(); if (command.equalsIgnoreCase("exit")) { System.out.print("종료합니다."); break; } if (command.startsWith("new ")) { processNewCommand(command.split(" ")); continue; } else if (command.startsWith("change ")) { processChangeCommand(command.split(" ")); continue; } else if (command.startsWith("list")) { processListCommand(); continue; }
list 명령어를 받았을 때 processListCommand() 메서드를 실행하도록 한다.
private static void processListCommand() { MemberListPrinter listPrinter = ctx.getBean("listPrinter", MemberListPrinter.class); listPrinter.printAll(); }
processListCommand() 메서드는
listPrinter
빈 객체를 불러와 printAll() 메서드를 실행한다.
MainForSpring을 실행하여 새로 추가한 기능을 사용해보자.
명령어를 입력하세요: new abc@def.com abc abc123 abc123 등록했습니다. 명령어를 입력하세요: new def@def.com def def123 def123 등록했습니다. 명령어를 입력하세요: list 회원 정보: 아이디=2, 이메일=def@def.com, 이름=def, 등록일=2023-06-16 회원 정보: 아이디=1, 이메일=abc@def.com, 이름=abc, 등록일=2023-06-16 명령어를 입력하세요:
정상적으로 작동하는 것을 확인할 수 있다.
세터 메서드를 이용한 의존 주입을 살펴보기 위해 지정한 회원 데이터를 콘솔에 출력하는 기능을 추가할 것이다.
MemberInfoPrinter.java✏️
package spring; public class MemberInfoPrinter { private MemberDao memberDao; private MemberPrinter printer; public void printMemberInfo(String email) { Member member = memberDao.selectByEmail(email); if (member == null) { System.out.println("데이터 없음\n"); return; } printer.print(member); System.out.println(); } public void setMemberDao(MemberDao memberDao) { this.memberDao = memberDao; } public void setPrinter(MemberPrinter printer) { this.printer = printer; } }
이 클래스는 지정한 이메일을 갖는 Member를 찾아서 정보를 콘솔에 출력한다.
AppCtx.java에 설정 추가✏️
AppCtx에 세터 메서드를 이용해 의존을 주입하는 설정 코드를 추가한다.
@Bean public MemberInfoPrinter infoPrinter() { MemberInfoPrinter infoPrinter = new MemberInfoPrinter(); infoPrinter.setMemberDao(memberDao()); infoPrinter.setPrinter(memberPrinter()); return infoPrinter; }
infoPrinter
빈이 세터 메서드를 이용해memberDao
빈과memberPrinter
빈을 주입하고 있다.MainForSpring.java에 코드 추가✏️
while (true) { System.out.println("명령어를 입력하세요: "); String command = reader.readLine(); if (command.equalsIgnoreCase("exit")) { System.out.print("종료합니다."); break; } if (command.startsWith("new ")) { processNewCommand(command.split(" ")); continue; } else if (command.startsWith("change ")) { processChangeCommand(command.split(" ")); continue; } else if (command.startsWith("list")) { processListCommand(); continue; } else if (command.startsWith("info ")) { processInfoCommand(command.split(" ")); continue; } printHelp(); }
info email 명령어를 받았을 때 processListCommand() 메서드를 실행하도록 한다.
private static void processInfoCommand(String[] arg) { if (arg.length != 2) { printHelp(); return; } MemberInfoPrinter infoPrinter = ctx.getBean("infoPrinter", MemberInfoPrinter.class); infoPrinter.printMemberInfo(arg[1]); }
processInfoCommand() 클래스는 콘솔로 입력받은 email을 인자로 printMemberInfo() 메서드를 실행한다.
MainForSpring을 실행하여 새로 추가한 기능을 사용해보자.
명령어를 입력하세요: new abc@def.com abc abc123 abc123 등록했습니다. 명령어를 입력하세요: new def@def.com def def123 def123 등록했습니다. 명령어를 입력하세요: info abc@def.com 회원 정보: 아이디=1, 이메일=abc@def.com, 이름=abc, 등록일=2023-06-16 명령어를 입력하세요:
정상적으로 작동하는 것을 확인할 수 있다.
회원 가입 프로젝트는 이렇게 3편으로 마무리 짓도록 하겠다. 하지만, 앞으로도 이 프로젝트의 코드를 이용해 여러 이론과 실습을 공부하도록 할 것이기 때문에 질리게 보게 될 것 같다.
이번 포스트에서도 추가적으로 DI의 두 방식을 다뤘듯이,
기본 데이터 타입 값 설정, 싱글톤 객체에 대한 설명, 여러 개의 설정 파일 사용법, 의존 자동 주입, DB 연동 등
앞으로도 다룰 것들이 많으니 차차 공부하도록 하겠다.
- 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 | 최범균님 저