티스토리 뷰
초보 웹 개발자를 위한 스프링 5 프로그램 입문을 보고 복습 겸 개인 학습 정리입니다. (windows 기준)
스프링의 특징
의존 주입 (DI) 지원
AOP (Aspect-Orignted Programing) 지원
MVC 제공
JDBC, JPA 연동 선언적 트랜젝션 DB 연동 지원
의존 (Dependency)
객체 간의 의존을 의미
한 클래스가 다른 클래스의 메서드를 실행할 때 이를 '의존'이라고 표현
의존은 변경에 의해 영향을 받는 관계를 의미
→ 기존 클래스 내부에서 직접 의존 객체를 생성하는 것이 쉬운 방법이지만 유지보수 관점에서 문제점을 유발할 수 있다.
그래서 Spring DI를 통한 의존 처리
DI이라는 방식을 이용하여 모듈 간의 결합도를 낮춘다.
대신 의존 객체를 전달받는 방식
의존 객체를 생성자를 통해 주입한다 <- 변경의 유연함
회원가입 및 수정 예제 프로젝트 만들기 (메이븐 기준) 예제 코드
sp5-chap03 프로젝트 폴더 생성
프로젝트(sp5-chap03) 하위 폴더로 src\main\java 생성
sp5-chap03 폴더 내부에 pom.xml 생성
※ 책에 나와있는 pom.xml파일은 메이븐 버전이 달라 프로그램이 실행이 안되니 현재 버전에 맞게 수정이 필요하다.
java 디렉토리에서 패키 지명을 spring으로 작성 후 클래스들을 만든다.
→ sp5-chap03/src/main/java/spring/각 클래스 명
회원 데이터 관련 클래스
- Member
- WrongIdPasswordException (패스워드
- MemberDao (db 연결 대신 java map을 이용하여 만든 클래스)
회원 가입 처리 관련 클래스
- DuplicateMemberException
- RegisterRequest
- MemberRegisterService
암호 변경 관련 클래스
- MemberNotFoundException
- ChangePasswordSecvice
객체 조립기 작성
객체 생성에 사용할 클래스를 변경하기 위해 객체를 주입하는 코드 한 곳만 변경하면 된다고 했다.
객체를 생성하고 의존 객체를 주입해주는 클래스를 작성하는 것.
의존 객체를 주입한다는 것은 서로 다른 두 객체를 조립한다고 생각할 수 있어 이 역할을 하는 것을 조립기 (assembler)라고 표현한다.
이 조립 키 클래스는 assembler 패키지를 따로 만들어 코드를 작성한다.
→ sp5-chap03/src/main/java/assembler/Assembler.java
package assembler;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberRegisterService;
public class Assembler {
private MemberDao memberDao;
private MemberRegisterService regSvc;
private ChangePasswordService pwdSvc;
//13~18행에서 객체에 대한 의존을 주입시킴
public Assembler() {
memberDao = new MemberDao(); // 만약 다른 클래스를 사용하려면 여기서 변경하면된다.
regSvc = new MemberRegisterService(memberDao);
pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao);
}
public MemberDao getMemberDao() {
return memberDao;
}
public MemberRegisterService getMemberRegisterService() {
return regSvc;
}
public ChangePasswordService getChangePasswordService() {
return pwdSvc;
}
}
메인 클래스 작성
메인 클래스는 명령어를 입력받고 각 명령어에 알맞은 기능을 수행하도록 구현되어있다.
new : 새로운 회원 데이터 추가
change : 회원 데이터의 암호를 변경
Main.java 38행
// 메인 클래스의 38행 발췌
private static Assembler assembler = new Assembler();
38행에서 Assembler 객체를 선언하고 생성했는데, 이때 Assembler 클래스의 생성자에서 필요한 객체를 생성하고 의존을 주입한다.
따라서 38행에서 Assembler 객체를 생성하는 시점에 사용할 객체가 모두 생성된다.
package main;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import assembler.Assembler;
import spring.ChangePasswordService;
import spring.DuplicateMemberException;
import spring.MemberNotFoundException;
import spring.MemberRegisterService;
import spring.RegisterRequest;
import spring.WrongIdPasswordException;
public class MainForAssembler {
public static void main(String[] args) throws IOException {
// BufferedReader (== Scanner) System.in이니까 프로그램에서 사용자로부터 입력받기 위해 초기화
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) { // while 문이라 exit입력하기 전까지 계속 실행됨
System.out.println("명령어를 입력하세요:");
String command = reader.readLine(); //위 19행에서 초기화했으니까 여기서 입력받음
if (command.equalsIgnoreCase("exit")) { //사용자가 exit입력하면 종료됨
System.out.println("종료합니다.");
break;
}
if (command.startsWith("new ")) { //입력한 문자열이 NEW로 시작되면 processNewCommand() 실행 "new"뒤에 공백문자가 있음
processNewCommand(command.split(" ")); // 이 command.split(" ") 코드는 command값이 "new a@a.com 이름 암호 암호"라면
continue; // command.split(" ") -> {"new", "a@a.com", "이름", "암호", "암호"} 이런 결과를 만들어 processNewCommand 전달한다.
} else if (command.startsWith("change ")) { //입력한 문자열이 change로 시작되면 processChangeCommand() 실행 "change로"뒤에 공백문자가 있음
processChangeCommand(command.split(" "));
continue;
}
printHelp(); //명령어를 잘못 입력한 경우 도움말 출력해주는 메서드 실행됨
}
}
//assembler 객체 생성
private static Assembler assembler = new Assembler();
private static void processNewCommand(String[] arg) {
if (arg.length != 5) {
printHelp();
return;
}
//Assembler.java에서 이미 의존을 주입했고 38행에서 Assembler를 생성했기 때문에 사용할 수 있다.
MemberRegisterService regSvc = assembler.getMemberRegisterService();
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 =
assembler.getChangePasswordService();
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();
}
}
[Run As] → [Java Application] 누르면 이클립스 콘솔 뷰에 아래와 같이 출력되면 잘 작성한 것
스프링 DI설정
스프링을 사용하지 않고 Assembler 클래스를 만들어 의존 주입을 이용했다면 스프링을 사용하는 코드를 작성해보자.
@Configuration 애너테이션과 @Bean 애너테이션을 붙이고 설정 클래스를 이용하여 컨테이너를 생성해야 한다.
config 패키지에 AppCtx 클래스 생성
→ sp5-chap03/src/main/java/config/AppCtx.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은 스프링 설정 클래스를 의미함 이 애노테이션을 붙여야 스프링 설정 클래스로 사용할 수 있다.
@Configuration
public class AppCtx {
// @Bean 애노테이션은 해당 메서드가 생성한 객체를 스프링 빈이라고 설정한다.
@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
해당 메서드가 생성한 객체를 스프링 빈이라고 설정한다
빈 애노테이션을 붙인 각각의 메서드마다 한 개의 빈 객체 생성
이때 메서드 이름을 빈 객체의 이름으로 사용한다.
getBean() 메서드를 이용해서 사용할 객체를 구할 수 있다.
위 설정 클래스를 만들고 난 뒤, 객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너이므로 설정 클래스를 이용해서 컨테이너를 생성해야 한다.
책 2장에서 사용한 AnnotationConfigApplicationContext 클래스를 이용해서 스프링 컨테이너를 생성할 수 있다.
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
다음으로 스프링 컨테이너를 생성하면 getBean() 메서드를 이용해서 사용할 객체를 구할 수 있다.
// 컨테이너에서 이름이 memberRegSvc인 빈 객체를 구한다.
MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);
→ 스프링 컨테이너인 ctx로부터 이름이 "memberRegSvc"인 빈 객체를 구한다.
DI 방식 1 : 생성자 방식 - 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.
spring 패키지의 MemberRegisterService 클래스의 코드를 보면 생성자를 통해 의존 객체를 주입받아 필드(this.memberDao)에 할당했다.
※ 생성자에 전달할 의존 객체가 두 개 이상이어도 동일한 방식으로 주입하면 된다.
package spring;
import java.time.LocalDateTime;
public class MemberRegisterService {
private MemberDao memberDao;
// 생성자를 통해 의존 객체를 주입 받음
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
public Long regist(RegisterRequest req) { //주입 받은 의존 객체의 메서드를 사용
Member member = memberDao.selectByEmail(req.getEmail());
if (member != null) {
throw new DuplicateMemberException("dup email" + req.getEmail());
}
Member newMember = new Member(
req.getEmail(),req.getPassword(), req.getName(), LocalDateTime.now());
memberDao.insert(newMember);
return newMember.getId();
}
}
그리고 config 패키지에 AppCtx 클래스에서 생성자를 이용해서 의존 객체를 주입하기 위해 해당 설정을 담은 메서드를 호출했다.
→ sp5-chap03/src/main/java/config/AppCtx.java
public class AppCtx {
// @Bean 애노테이션은 해당 메서드가 생성한 객체를 스프링 빈이라고 설정
@Bean // 이 빈은 각각의 빈 객체를 생성
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao());
}
...이하 생략
AppCtx 클래스에 세터 메서드 방식을 사용하는 설정을 추가했으므로, MainForSpring 코드에 MemberInfoPrinter 클래스를 사용하는 코드를 추가한다.
→ sp5-chap03/src/main/java/main/MainForSpring .java
package main;
... 생략
import spring.MemberListPrinter;
... 생략
public class MainForSpring {
private static ApplicationContext ctx = null;
// AnnotationConfigApplicationContext 사용해서 스프링 컨테이너 생성, 객체 생성하고 의존 객체 주입
public static void main(String[] args) throws IOException {
//설정파일 AppCtx클래스로 부터 생성할 객체와 의존 주입 대상을 정함
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.println("종료합니다.");
break;
}
if (command.startsWith("new ")) {
processNewCommand(command.split(" "));
continue;
} else if (command.startsWith("change ")) {
processChangeCommand(command.split(" "));
continue;
} // 추가 회원 list를 볼수 있는 명령어 추가
else if (command.equals("list")) {
processListCommand();
continue;
}
printHelp();
}
} // list를 볼 수 있는 메서드 추가
private static void processListCommand() {
MemberListPrinter listPrinter =
ctx.getBean("listPrinter", MemberListPrinter.class);
listPrinter.printAll();
... 생략
[Run As] → [Java Application]
추가한 명령어 list를 입력하면 입력한 회원의 정보가 출력되는 것을 확인할 수 있다.
DI 방식 2 : 세터 메서드 방식 - 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.
생성자 외 세터 메서드(get 게터와 set 세터.. )를 이용해서 객체를 주입받기도 한다.
메서드는 자바빈 규칙에 따라 작성한다.
- 메서드 이름은 set으로 시작
- set 뒤에 첫 글자는 대문자로 시작
- 파라미터 1개
- 리턴 타입 void
회원 정보를 보기 위한 추가 클래스 작성한다.
spring패키지에 MemberInfoPrinter클래스 생성
→ sp5-chap03/src/main/java/spring/MemberInfoPrinter.java
18행~24행~ 에 사용된 이 세터 메서드는 MemberDao 타입의 객체와 아래 MemberPrinter 타입의 객체에 대한 의존을 주입하기 위해 사용되었다.
package spring;
public class MemberInfoPrinter {
private MemberDao memDao;
private MemberPrinter printer;
public void printMemberInfo(String email) {
Member member = memDao.selectByEmail(email);
if (member == null) {
System.out.println("데이터 없음");
return;
}
printer.print(member);
System.out.println();
}
// 두 개의 세터 메서드 정의
// 이 세터 메서드는 MemberDao 타입의 객체와 아래
// MemberPrinter 타입의 객체에 대한 의존을 주입하기위해 사용됨
public void setMemberDao(MemberDao memberDao) {
this.memDao = memberDao;
}
public void setPrinter(MemberPrinter printer) {
this.printer = printer;
}
}
config 패키지에 AppCtx 클래스에 @Bean 객체를 추가한다
→ sp5-chap03/src/main/java/config/AppCtx.java
추가된 infoPrinter() 빈은 세터 메서드를 이용해서 memberDao 빈과 memberPrinter 빈을 주입한다.
// DI 세터 메서드 추가 기입
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
infoPrinter.setMemberDao(memberDao());
infoPrinter.setPrinter(memberPrinter());
return infoPrinter;
}
AppCtx 클래스에 세터 메서드 방식을 사용하는 설정을 추가했으므로, MainForSpring 코드에 MemberInfoPrinter 클래스를 사용하는 코드를 추가한다.
기존 명령어 new, change 외 list, info를 추가하였다.
→ sp5-chap03/src/main/java/main/MainForSpring. java
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("명령어를 입력하세요:");
String command = reader.readLine();
if (command.equalsIgnoreCase("exit")) {
System.out.println("종료합니다.");
break;
}
if (command.startsWith("new ")) {
processNewCommand(command.split(" "));
continue;
} else if (command.startsWith("change ")) {
processChangeCommand(command.split(" "));
continue;
} // 추가 회원 list를 볼수 있는 명령어 추가
else if (command.equals("list")) {
processListCommand();
continue;
} // DI 세터 방식 info 명령어 추가
else if (command.startsWith("info ")) {
processInfoCommand(command.split(" "));
continue;
}
... 생략
// 추가 회원 list를 볼수 있는 메서드
private static void processListCommand() {
MemberListPrinter listPrinter =
ctx.getBean("listPrinter", MemberListPrinter.class);
listPrinter.printAll();
}// DI 세터 방식 info 명령어 추가
private static void processInfoCommand(String[] arg) {
if (arg.length !=2) {
printHelp();
return;
}
MemberInfoPrinter infoPrinter = ctx.getBean("infoPrinter", MemberInfoPrinter.class);
infoPrinter.printMemberInfo(arg[1]);
}
[Run As] → [Java Application]
DI방식 1에서 추가한 list와 DI방식 2에서 추가한 info 명령어를 입력하면 입력한 회원의 정보가 출력되는 것을 확인할 수 있다.
DI방식 장점을 정리하면,
생성자 방식 : 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.
설정 메서드 방식 : 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.
기본 데이터 타입 값 설정
추가로 이 프로그램? 의 버전을 알 수 있는 명령어를 추가해보도록 하자.
새로운 VersionPrinter 클래스를 작성한다.
단 아래 코드는 두 개의 int 타입 값을 세터 메서드로 전달받는다.
→ sp5-chap03/src/main/java/spring/VersionPrinter. java
package spring;
public class VersionPrinter {
private int majorVersion;
private int minorVersion;
public void print() {
System.out.printf("이 프로그램의 버전은 %d.%d입니다. \n\n", majorVersion,minorVersion);
} // int 타입 값을 세터 메서드로 전달 받음
public void setMajorVersion(int majorVersion) {
this.majorVersion = majorVersion;
} // int 타입 값을 세터 메서드로 전달 받음
public void setMinorVersion(int minorVersion) {
this.minorVersion = minorVersion;
}
}
config 패키지에 AppCtx 클래스에 @Bean 객체를 추가한다
→ sp5-chap03/src/main/java/config/AppCtx.java
// 기본 버전 타입값 추가 기입
@Bean
public VersionPrinter versionPrinter() {
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(5);
versionPrinter.setMinorVersion(0);
return versionPrinter;
}
AppCtx 클래스에 빈 객체를 추가했으므로 MainForSpring 코드에 VersionPrinter 관련 코드를 추가한다.
→ sp5-chap03/src/main/java/main/MainForSpring. java
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("명령어를 입력하세요:");
String command = reader.readLine();
if (command.equalsIgnoreCase("exit")) {
System.out.println("종료합니다.");
break;
}
if (command.startsWith("new ")) {
processNewCommand(command.split(" "));
continue;
} else if (command.startsWith("change ")) {
processChangeCommand(command.split(" "));
continue;
} // 추가 회원 list를 볼수 있는 명령어 추가
else if (command.equals("list")) {
processListCommand();
continue;
} // DI 세터 방식 info 명령어 추가
else if (command.startsWith("info ")) {
processInfoCommand(command.split(" "));
continue;
} // 버전 명령어 추가
else if (command.equals("version")) {
processVersionCommand();
continue;
}
printHelp();
}
}
// 추가 회원 list를 볼수 있는 메서드
private static void processListCommand() {
MemberListPrinter listPrinter =
ctx.getBean("listPrinter", MemberListPrinter.class);
listPrinter.printAll();
}// DI 세터 방식 info 명령어 추가
private static void processInfoCommand(String[] arg) {
if (arg.length !=2) {
printHelp();
return;
}
MemberInfoPrinter infoPrinter = ctx.getBean("infoPrinter", MemberInfoPrinter.class);
infoPrinter.printMemberInfo(arg[1]);
}
// 버전 명령어 추가
private static void processVersionCommand() {
VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
versionPrinter.print();
}
[Run As] → [Java Application]
실행하면 Version 명령어에 대한 내용이 출력되는 것을 확인할 수 있다.
☆ 두 개 이상의 설정 파일 사용하기 ☆
위에서 작성한 예제 외 실제로 스프링을 이용해서 개발하다 보면 많은 빈을 설정하게 된다.
설정하는 빈의 개수가 증가하면 한 개의 클래스 파일에 설정하는 것보다 영역별로 설정 파일을 나누면 관리하기 편해진다.
스프링은 한 개 이상의 설정 파일을 이용해서 컨테이너를 생성할 수 있는데,
예제 파일을 두 개 만들어보자.
Config 패키지에 예제 파일을 2개 만들어 추가한다.
→ sp5-chap03/src/main/java/config/AppConf1.java
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.MemberDao;
import spring.MemberPrinter;
// 스프링 컨테이너 설정 애노테이션
@Configuration
public class AppConf1 {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
}
→ sp5-chap03/src/main/java/config/AppConf2.java
package config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberInfoPrinter;
import spring.MemberListPrinter;
import spring.MemberRegisterService;
import spring.VersionPrinter;
import spring.MemberPrinter;
@Configuration
public class AppConf2 {
@Autowired // 자동 주입 기능을 위한 것 MemberDao 타입의 빈을 memberDao 필드에 할당
private MemberDao memberDao;
@Autowired // 자동 주입 기능을 위한 것 MemberPrinter 타입의 빈을 memberPrinter 필드에 할당
private MemberPrinter memberPrinter;
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao);
}
@Bean
public ChangePasswordService changePwdSvc() {
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao);
return pwdSvc;
}
@Bean
public MemberListPrinter listPrinter() {
return new MemberListPrinter(memberDao, memberPrinter);
}
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
infoPrinter.setMemberDao(memberDao);
infoPrinter.setPrinter(memberPrinter);
return infoPrinter;
}
@Bean
public VersionPrinter versionPrinter() {
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(5);
versionPrinter.setMinorVersion(0);
return versionPrinter;
}
}
@Autowired
자동 주입 기능을 위한 것. 의존 주입과 관련이 있다.
스프링 설정 클래스의 필드에 이 애노테이션을 붙이면 해당 타입의 빈을 찾아 필드에 할당한다.
위 코드의 경우 스프링 컨테이너는 MemberDao 타입의 빈을 memberDao 필드에 할당한다.
AppConf1 클래스의 MemberDao 타입의 빈을 설정했으므로 AppConf2 클래스의 memberDao 필드에는 AppConf1 클래스에서 설정한 빈이 할당된다..
<- 어렵다.. chap 4장에서 ㅠㅠ 계속
'study > Spring' 카테고리의 다른 글
chap 07 - AOP 프로그래밍 (0) | 2023.01.10 |
---|---|
chap 06 - 빈 라이프사이클 & 범위 (0) | 2023.01.09 |
chap 05 - 컴포넌트 스캔 (0) | 2023.01.05 |
chap 04 - 의존 자동 주입 (0) | 2023.01.04 |
환경 변수 설정 (windows 기준으로 설치) (0) | 2022.11.29 |
- Total
- Today
- Yesterday
- 커맨드 객체
- django
- django.contrib.auth
- 웹페이지
- 암호화
- API
- Django tutorial
- path variable
- Magazine K
- 혼자 공부하는 파이썬
- git공부
- Java
- authenticate()
- 톰캣
- 배열
- git
- Python
- 디자인 패턴
- error: failed to push some refs to 'https://github.com/
- 회원가입
- 검색 결과 내 페이지네이션
- git 공부
- Spring
- django-environ
- 환경 변수 설정
- 한글 형태소 분석기
- 면접을 위한 CS 전공 지식 노트
- python3
- musma
- 회원 로그인
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |