스프링은 ApplicationContext라는 IOC 컨테이너를 사용합니다. 이 컨테이너 덕분에 우리는 모든 API에 대해 설정 구성을 일일히 해 주지 않아도 됩니다. 또, 빈의 생성이나 관계를 설정해 주는 일에 대해 해방될 수 있고, 결국 과거에 위와 같은 코드를 짰던 시간만큼 조금 더 빠른 웹 어플리케이션을 만들 수 있습니다. 또한 확장성도 높아집니다.
DI
DI는 IOC와 함께 스프링의 핵심 개념 중 하나입니다. 세션에서 배웠듯 우리가 singleton 패턴을 사용해 stateless하게 설계해야하는 것들이 많습니다. 그러나 우리가 배운 SOLID 원칙을 지키려고 하거나 DI없이 api를 설계할 때면, 이러한 원칙들이 부딪히는 경우가 종종 등장합니다. DI는 이것들을 해결해 줍니다.
영속성과 관련된 다양한 인터페이스
스프링은 영속성과 관련된 다양한 라이브러리를 지원합니다. 데이터베이스의 가장 핵심적인 원칙은 ‘무결성’이라고 배웠습니다. 그 말마따나 데이터베이스에는 아무 데이터나 집어넣으면 안됩니다. 그러나 스프링은 자바의 (때로는 너무 깐깐한) 데이터 타입의 이점을 십분 활용해서, 그에 맞게 개발된 다양한 라이브러리를 제공하여 데이터베이스와 관련된 행위들을 사용자가 안심하고 진행할 수 있게 합니다.
스프링에서 DI (의존성 주입) 를 사용하는 이유가 무엇일까?
항해와 뗄레야 뗄 수 없는 기술매니저, 담임매니저로 예를 들어보겠습니다.
우리 C반이 하는 모든 작업을 코드로 작성한다고 가정했을 때,
우리 11 → 9조는 이번 주를 김태현 기술매니저님, 송민진 담임매니저님과 함께 하고 있죠?
그럼 상담, 시험, 공지안내 등은 송민진 인스턴스를 생성해서 코드를 작성해야 할 것이고
또 기술매니저 순회등은 김태현 인스턴스를 생성해서 코드를 작성해야 할 것입니다.
만약 9조 주차별 조 평가형식이 있다면 송민진 인스턴스와 김태현 인스턴스를 둘 다 생성해서 코드를 작성해야 합니다.
그런데 만약 송민진 매니저님이 아침에 저를 깨우다 지쳐서 담임매니저를 때려친다면?
우리가 작성한 모든 코드에서 송민진 매니저님을 지우고 새로운 매니저 인스턴스를 생성해주는 코드로 바꿔야 할 것입니다.
또 만약 다음 주차로 가서 김태현 매니저님이 아닌, 진유진 기술매니저님으로 바뀐다면?
수많은 코드들을 매주 지우고 매주 새로 짜야 할 것입니다.
그러나 인스턴스 생성 방식이 아닌, 매니저 인터페이스를 주입받고 맨 마지막에
담임매니저 = 송민진, 기술매니저 = 김태현으로 구체화해 준다면?
아 그냥 상상만 해도 누가 매니저하든 상관이 없을 것입니다.(제 의견 아님)
그래서 벌써 이렇게만 해도 두 가지 이점이 생깁니다.
수많은 코드를 덜 교체해도 됨
코드를 처음 보는 사람은 누가 기술매니저인지 누가 운영매니저인지 뭔 역할을 하는지 알수가없음
ORM, JPA, 그리고 Spring Data JPA
**ORM**은 객체(Instance)와 관계형 데이터베이스(RDBMS)의 데이터를 자동으로 매핑해주는것을 말합니다. 서블릿 컨테이너가 Web Request와 Servlet Component를 자동으로 매핑해주듯이요. 그래서 Object Relational Mapping입니다.
근데 생각해보니 이상하지 않나요?
Web Request가 “localhost:8080/api/hanghae”로 GET 메서드를 통과한다 치면 서블릿 컨테이너가 어 그래 /api/hanghae구나 어 그래 GET Method구나 해서 연결시켜주면 된다고 치고 (요청과 응답의 url type은 String으로 동일하니까)
@RestController
@RequestMapping("/api")
public class HangHaeController{
@GetMapping("/hanghae")
public String cry(){
return "응애"
}
}
우리는 자바 객체를 쓰니까 class를 사용하고, RDBMS는 table을 사용하는데(객체 모델과 관계형 모델 간의 불일치) 이 친구가 어떻게 알고 맵핑을 시켜주는 걸까요?
**JPA**라는 친구가 무한SQL 마법을 써서 알아서 해주는 거랍니다.
JPA 안의 JDBC API라는 친구가 SQL문을 써서 알아서 DB에 접근을 해주게요.
우리는 이제 JOIN문을 안쓰고도 관계 Annotation만 써주면 JPA가 알아서 이 문제를 해결 해 줍니다.
JPA의 장점이 뭘까요? 며칠전에 in-line memory를 써서 h2 데이터베이스로 데이터를 삽입해줬습니다.
근데 문제점이 뭐냐 : 끄면 내 데이터 다 사라짐, 누가 내 데이터 저장된거 보여주세요 하면 다시 SQL문써서 데이터 만들고 JOIN 쿼리로 데이터베이스의 table key값 연동시켜주는 수고를 해야함
그래서 h2같은 데이터베이스가 종료되고 걍 컴 껐다키고 U+가 내 인터넷을 조졌다가 다시 연결시켜줘도 알아서 관계가 맺어질 수 있도록 객체-테이블에 매핑을 해 놓은것입니다.
⇒ 그래서 휘발성이 아닌 영속성(Persistant) 이라는 말을 쓰고, 이걸 자바에서 제공한다고 해서
Java Persistant Api라는 단어가 나오게 된거죠.
또 여기 JPA와 다른 ORM인 MyBatis의 비교도 있습니다.
그럼 **Spring Data JPA**란 무엇일까요?
아까 위에서 제가 스프링 프레임워크의 장점에 대해서 작성할 때, 스프링 프레임워크는 영속성 데이터베이스와 관련된 다양한 인터페이스와 라이브러리를 지원하는 장점이 있다고 썼습니다.
이 Spring Data JPA는 JPA와 관련된 스프링에서 제공하는 모듈입니다.
이번주차 항해 인강을 들을 때, 요청이 들어오면 Entity Manager Factory라는곳에서 노예(EntityManager)들을 만들어서 이 친구들이 DB connection pool에 접근해서 DB를 건드린다는 설명을 들으셨을 겁니다.
그런데 우리가 지난주차에 숙제로 제출한 코드를 봅시다.
<Repository>
public interface LetterRepository extends JpaRepository<Letter, Long> {
List<Letter> findAllByOrderByModifiedAtDesc();
}
<Service>
@Service
@RequiredArgsConstructor
public class LetterService {
private final LetterRepository letterRepository;
@Transactional(readOnly = true)
public List<LetterResponseDto> findAll(){ // service Layer에서 Entity >> DTO 변환작업, 사유 : LazyInitializationException 위험부담 줄임
return letterRepository.findAllByOrderByModifiedAtDesc().stream().map(LetterResponseDto::new).collect(Collectors.toList());
}
}
우리는 EntityManager를 쓴 기억조차 없죠. 왜일까요? 우리가 Extend한 JpaRepository에 그 답이 있습니다.
package org.springframework.data.jpa.repository.support;
import ...
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final EntityManager em;
public Optional<T> findById(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
Class<T> domainType = getDomainClass();
if (metadata == null) {
return Optional.ofNullable(em.find(domainType, id));
}
LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}
// Other methods...
}
JPA Repository에서 이런 엔티티 코드를 다 만들어놓고 우리에게 적당한 타입과 ID만 넣어 주면 이 메소드를 자동으로 오버라이딩하게 만들어놔서 그렇습니다.
Spring Data JPA가 없었다면? 우리는 저 한줄로 끝낸 findAll 메소드를 다음과 같이 써야 할 것입니다.
public abstract class GenericDAOImpl<T, id> implements GenericDAO<T, id> {
private Class<T> type;
@PersistenceContext
protected EntityManager entityManager;
public GenericDAOImpl(Class<T> type) {
super();
this.type = type;
}
... save and delete classes go here
@Override
public List<T> findAll(T2 where) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(type);
Root<T> rootQuery = criteriaQuery.from(type);
if (where != null) {
EntityType<T> entity = entityManager.getMetamodel().entity(type);
SingularAttribute<? super T, ?> attribute = null;
for (SingularAttribute<? super T, ?> singleAttribute: entity.getSingularAttributes()) {
// loop through all attributes that match this class
if (singleAttribute.getJavaType().equals(where.getClass())) {
// winner!
attribute = singleAttribute;
break;
}
}
// where t.object = object.getID()
criteriaQuery.where(criteriaBuilder.equal(rootQuery.get(attribute), where));
}
criteriaQuery.select(rootQuery);
TypedQuery<T> query = entityManager.createQuery(criteriaQuery);
// need this to make sure we have a clean list?
// entityManager.clear();
return query.getResultList();
}
Board의 Entity 안에는 많은 정보가 들어간다. PK인 id값을 포함해 제목, 내용, 작성자 등도 들어가지만, 가장 중요한 게시물을 지울 수 있는 password값이 들어가있다. password값까지 response로 리턴해줄 필요도 없고, 또한 누가 접근했을 때 한단계 더 숨겨줄 수 있기 때문이다.(Encapsulism과 비슷한 맥락이라고 보면 된다.)
이 Letter를 list로 만들어서 반환하는 것이 아니라,
이렇게 ResponseDto에 담아 주는것이 현명하다.
그럼 Letter를 ResponseDto로 바꿔 주어야 하는데, 어떻게, 또 어느 Layer에서 변환을 해 주어야 할까?
Controller Layer에서 변환해주어도 괜찮지만, 보통은 Service Layer에서 변환작업을 해 주는게 좋기도 하고, flow에 맞다.
그럼 Entity를 DTO로 어떻게 바꿔줄까?
내가 알아본 방법은 두 가지였다.
1. Stream을 이용해 바꿔주기
2. Array로 만들고 List로 바꿔주기
그 중 Stream을 이용하기로 했다.
이렇게 자바8의 collection을 사용해서 바꾸어 주었다.
LetterController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class MainController {
private final LetterService letterService;
@GetMapping("/posts")
public List<LetterResponseDto> viewAll(){
return letterService.findAll();
}
@PostMapping("/post")
public Long postLetter(@RequestBody LetterRequestDto letterRequestDto){
return letterService.postLetter(letterRequestDto);
}
}
여기서 @RestController란 무엇일까?
@RestController = @Controller + @ResponseBody이다.
@Controller 어노테이션은 기존의 컨트롤러 어노테이션과 똑같고,
@ResponseBody 어노테이션은 return해주는 객체를 MessageConverter를 통해 매핑해서 본문에 담아 클라이언트에 전달한다.
처음에는 RequestBody와 헷갈려서 그대로 전달했다가 에러가 나버렸다.
그래서 Controller에 @RequestBody를 다 붙여주었다.
그랬더니 또 문제가 났다.
api 명세서에는 password만 들고 들어와야 하는 것이다.
보통 우리가 게시판에서 글을 지우려면 pop-up창에 password 하나만 치면 삭제가 되는 구조를 많이 이용한다.
그래서 이 password값만 받아오고 싶은데, 도저히 RequestDto에 있는 password값을 가져와서 다시 확인하기는 싫었다.
나중에 상세 페이지에 들어가서 삭제를 누르려면 RequestDto를 이용하면 되겠지만, 상세 페이지 밖에서 글을 삭제하려면
RequestDto에 있는 타이틀, 제목같은 not null값을 다 채워줘야 하기 때문이다.
한참을 500에러와 씨름하다가 다음과 같은 깨달음을 얻었다.
@RequestBody
RequestBody는 요청값을 json type로 받겠다고 선언하는 어노테이션으로, 이 어노테이션 덕분에 우리는 DTO 형태로 값을 가져올 수 있다. Spring은 Controller로 요청을 받을 때 jackson이라는 내장 라이브러리에 있는 MappingJackon2HttpMessageConverter를 이용해 요청값으로 들어온 json형태의 데이터를 DTO에 맞는 형태로 파싱해주게 된다. 그래서 우리는 스프링을 통해 json 형태를 편리하게 DTO라는 객체로 받아올 수 있는 것이다.
그래서 이 JSON 형태의 글이 @Responsebody 어노테이션을 통해
RequestDto requestDto = new RequestDto(title,content,author,password)
형태로 자동 바인딩되어 저장된다.
이를 "역직렬화 / DeSerialization"이라고 부른다! (Singleton패턴에서 왜 이런 개사기스킬을 만들어놨는지 궁금했는데 여기서 깨달았다.)
역직렬화는 생성자를 거치지 않고, Java의 Reflection을 통해 객체를 정의하는 매커니즘이다. 직렬화 가능한 클래스들은 기본 생성자가 항상 있어야 하기 때문에 RequestDto에 기본 생성자가 정의되어있지 않으면 데이터 바인딩에 실패한다.
아무튼 나는
@DeleteMapping("/post/{id}") //RequestBody 방식으로 구현, body : raw - text
public String deleteLetter(@PathVariable Long id, @RequestBody String password) throws JSONException {
return letterService.deleteLetter(id, password);
}
이렇게 RequestBody 어노테이션 뒤에 String password를 선언하였기 때문에 JSON타입의 password가 내가 생각한 JSON객체로 들어오지않고
String password = “{\“password\“:\“1234\“}”;
와 같은 형태로 들어왔던 것이다.
그럼 어떻게 하는데!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
방법은 3개가 있다.
1. 죽어도 RequestBody를 쓰고 싶으면 @RequestBody JSONObject password로 받기
2. @ModelAttribute를 쓰기 : ModelAttribute는 RequestBody나 RequestParam을 선언하지 않으면 디폴트로 들어가기 때문에
@DeleteMapping("/post/{id}") //ModelAttribute 방식으로 구현, body : x-www.form-urlencoded
public String deleteLetter(@PathVariable Long id, String password) throws JSONException {
return letterService.deleteLetter(id, password);
}
이렇게 선언해서 나중에 body타입으로 받아주기
3. @RequestParam 사용하기 : RequestParam은 1개의 parameter를 받기 위해 사용되고있다. null 들어가면 안된다.
@DeleteMapping("/post/{id}") //REQUESTPARAM 방식으로 구현, body : form-data
public String deleteLetter(@PathVariable Long id, @RequestParam String password, Model m) throws JSONException {
m.addAttribute("password",password);
return letterService.deleteLetter(id, password);
}
느낀점
정말 맛있는 과제였다. 덕분에 정말 HTTP request, response, 그리고 MVC패턴과 각종 어노테이션에 대해서 학습할수 있는 좋은 기회였다....
그런데 내가 만들어야 하는것은 FE에서 준 query를 가지고 youtube에 get 방식으로 요청을 보내서 받은 response를 파싱해서 다시 FE로 보내주어야 프론트에서 그것을 렌더링할 수 있고, 그 기능을 실제로 구현해야 한다!
youtube api를 쓰면 쉽게 파싱할 수 있지만, 화이트카드를 계속 발급받으면서 Auth를 하는데 번거로움이 있어 그냥 대가리 박고 파싱하기로 했다.
문제 발생
근데 이거 어케함?
8080에 요청이 들어올때 response를 쥐어주는 법, request로 파싱하는 법은 알았다.
근데 내가 클라이언트 입장에서 다른 서버에 요청을 보내고 받아오는 것은 한번도 해보지 않은 기술이라 벌벌 헤멨다.
어찌저찌 유튜브에서 받아온 html(유튜브는 SSR 방식을 이용하기 때문에, 쉽게 말하면 빈 템플릿을 보내고 그 안에 이런저런 요청받은 정보들을 가져와서 렌더링한다). 은 또 어떻게 파싱할꼬...
시도해본 것들
일단 처음으로 youtube에 get 메서드로 통신하기 위해서는 여러 방법이 있었다.
HttpUrlConnection 사용하기 -> 너무 옛날 기술이라 요즘엔 잘 사용하지 않는다고 함
RestTemplate -> Spring 3 이후에 나온 기술, 현재는 또 Depricated당했다.
WebClient -> Spring 5 이후에 나온 기술
그중에 RestTemplate를 사용해보기로 했다.
UriComponentsBuilder를 이용해 Uri에 쿼리를 담아 보냈는데,
텅 빈 ResponseBody만 온다..
다음 코드의 문제점은 뭘까?
8080포트에서는 200번대로 다 떴는데, status code를 로그로 찍어보니 300번대가 떴다.
왜 301이 떴지? 하면서 온갖 생각을 다 하다가 하나를 생각해냈다.
HTTPS
HTTPS 프록시 기술을 사용하기 때문에 리다이렉팅을 하는 과정에서 불러오지 못했던 것이었다.
이걸가지고 3~4시간을 헤메다니...
아무튼 성공했고, 다음과 같은 responseBody를 받았다.
이거를~ 어떻게~ 파싱할까요~?
이 긴 String 사이에 JSON이 숨어있다. 그 숨겨진 JSON을 꺼내서 우리가 원하는 타이틀로 바꿀것이다.
jsoup은 html parser라 html이 변동되면 사용할 수 없기 때문에
youtube api를 쓰지 않으려면 일일히 파싱을 해 주어야 한다. (CSR은 깡통 html만 주기 때문에 jsoup을 쓸 수 없다.)
파싱은 JSONParser를 이용해서 파싱하려고 했다.Json.simple 라이브러리를 이용해서 바꾸어 주려고 하니
이 안에 파싱을 할 때 JSONObject로 파싱되는지, JSONArray로 파싱되는지에 따라 계속 확인해주어야 하고, 계속 형변환을 해 주었다.
JSONParser를 쓰는 레퍼런스들은 많아서 찾아볼만했지만 보통은
[{곤충1 : 잠자리}, {곤충2 : 매미}] 등의 단순화된 스트링에서만 파싱을 했고,
{asdfasdfdsfa:{asdfsdafsdfa{asdfdsaffdsafds:gdfadsaffdsafsadfdsa:sdaffsadfsadfdas:···}···}···}등의 구조를 알아보기 힘든 긴 JSON을 파싱하는 레퍼런스들은 없었기에, 일일히 해 주어야 했는데,
이 문장을 1억줄 쓰려니 생각만 해도 아찔했다.
바로 ObjectMapper의 등장이다.
ObjectMapper에서 readTree로 데이터를 읽으면 JsonNode로 반환한다. 더욱이나 좋은것은, get 메서드 안에 index와 fieldname을 둘 다 혼용해서 쓸 수 있기에 형변환을 안해주어도 된다는 점이 매우매우 마음에 들었다.
알게 된 것
머리를 박으면서 코딩을 하다보니 알게된것들이 있었다.
1. BE api의 flow
MusicParsercontroller에서 MusicParserService를 const injection을 통해서 주입하고 GetMapping으로 /search에서 FE에서 날려준 request를 가지고 MusicparserService.method(request)한다는 것
알고리즘 주차도 정말 힘들지만, 개인적으로는 정말 존경하는 김영한 선생님이 말씀하셨던 정말 잘하는 주니어의 특징인 '야생형'이 그리워졌다. 마침 내가 정말 존경하기도 하고 가장 번뜩이는 아이디어를 가졌다고 생각하는 프론트엔드 친구가 프로젝트를 추천했고, 같이 하게 되었다.
거창한 프로젝트는 아니고 2명이서 하기에 사실 git flow나 개발 프로세스를 익히기엔 턱없이 부족하지만, 실전 프로젝트 대비를 하는 입장에서는 정말 좋다고 생각했다.
늘 어디서 받은 js/html로 렌더링을 하며 혼자 개발 공부를 해왔지만, 이번에 처음으로 다른 언어와 같이 소통하고 정보를 주고받는다는것이 나를 두근두근하게 만들었다.
문제 발생
혼자하는 개발은 나혼자 정보를 주고받지만, '협업'은 다른 '사람'과 정보를 주고받을 수 있어야 한다.
FE는 3000 포트를 가져가고, 스프링부트는 8080부트를 가져간다.
그 사이에서 정보를 주고받아야 하는데, 처음부터 갑자기 너무 막막해졌다.
"프로젝트 생성은 했지만, 어떻게 해야 정보를 주고받을 수 있을까?"
혼자 인강을 보고 혼자 개발을 하면 8080포트만 사용하면 되는데, 3000 포트와는 어떻게 통신하는지를 몰랐다.
기능 개발에 앞서 리액트와 스프링부트 연동 과정부터 진행해야 했는데, 해본적도 없고 지금은 협업 주차도 아니니
BDD(Blog-Driven-Development)를 해야 했다. 그러나 BDD에는 큰 문제가 있었는데, 각 블로그마다 문제는 같아도 해결하는 방식이 천차만별이었다.
시도해본 것들 ( 백엔드 입장) : CORS 오류가 뜰 것을 대비하고 작성함
1. 먼저 Controller를 만든다
RequestMapping, GetMapping은 실험하기 위해 일부러 다르게 설정해 보았다.
2. setProxy.js파일을 생성해본다
Origin 구성요소가 하나라도 다르면 CORS 에러가 발생한다고 해서 미들웨어 설치도 해 보았다.
3.app.tsx에 axios과 관련된 설정 import해주고,
4. WebConfig 설정파일 만들기
이렇게 했더니 3000 포트에서 8080 포트의 내용을 받아올 수 있었다.
3000포트에서 "Hello MeoHyun"을 받아왔을 때의 그 쾌감은 처음에 Hello World를 쳤을때보다 쾌감이 30배 이상이었다.
그런데 문제가 생겼다.
나는 되는데 프론트단에서는 접속이 불가능했다.
찾아보니 이 에러는 IP주소가 비공개로 설정되어있을 경우 값을 할당받지 못해서 생기는 에러라고 한다.
생각해보니 나는 노트북에서 wi-fi 환경으로, FE는 랜으로 접속해서 오류가 터지는 것이었다.
그래서 root directory에 .env 파일을 생성해서
DANGEROUSLY_DISABLE_HOST_CHECK=true
이 한줄만 넣어주니 말끔히 완료되었다.
나중에 다른 백엔드에 물어보니, package.json에 프록시 코드 하나만 추가해주면 된다고 하였다.
협업이라는게 그냥 같이 코드만 만지고 하는건 줄로만 알았는데, 생각보다 복잡했다. 그리고 배울게 너무나 많아졌다.
객체 지향적 언어란 말 그대로 '지향'일 뿐이다. 객체 지향적 언어에 정답은 없다고 생각한다.
객체 지향에 다가가고 있어도, '이것이 객체 지향이다'는 말은 섣불리 할 수가 없었다.
SOLID 원칙을 배우면서 객체를 어떻게 구성해야 하는지에 대해서도 조금은 알 수 있었지만,
내가 앞으로 짜는 코드에 대해서 어떻게 객체지향적으로 짜야하는지는 아직 너무 이르기도 하고 디자인 패턴들을 많이 배워야 할 것 같다. JVM도 마찬가지다. CS지식이 전무한 내게 JVM의 시스템 구조를 조금 본 것은
혀에 잠깐 한 방울 닿은 정도로만 다가왔다.
그래서 이번주는 솔직히 점수를 많이 줄 수 없다고 생각한다.
조금 더 과장을 보태면 0점을 주고 싶다.
느낀 점
아는 것을 다시 공부하기는 굉장히 힘들다. 자바의 정석을 몇번이나 봐 온 나로써는 솔직히 조금 지루했다.
문제는 지루하면 안 된다는 것에 있다. 볼때마다 새로운 지식이 나오는데도 불구하고 무엇에 우선순위를 두어야 하는지에 대해 계속 뺑뺑 돌았다. 알고리즘 공부하다가 자바보다가 새로운 지식 보다가 하니 결국 남은게 없었다.
아는 형에게 정말 좋은 제안이 왔다. 스프링 개발을 부로, swift를 주로 하는 곳인데, 과장님이 나를 좋게 보신다고 이력서를 한 번 내보라고 했었는데, 그것에 대해 정말 고민했다.
그리고 나서 며칠 뒤 밤에, 같이 개발자를 목표로 했던 동료에게도 전화가 왔다. 면접을 봤었는데 스프링은 왜 쓰는지, 또 다른 개념에 대해선 무엇을 알고 있는지를 물어봤다고 했다. 곰곰히 생각해 봤는데 내가 거기에 대해서 답변할 수 있는게 너무 적었다. 그래서 부끄러워서 아는 형에게 조금 더 공부해야 할 것 같다고 연락을 남겼다.
이 상태로 취업을 했다간 나보다 나를 소개시켜준 형에게 부끄러워서 회사를 다니지 못할것 같았다. 취업시켜준다는 말은 안했지만... 정말 많은 것을 보고 듣고 알고 가야겠다고 생각했다.
다음 주 목표
하나에 몰두하지 않고 여러개를 돌린 시도 자체는 좋으나 결과가 너무 참담했다. 조금 더 개념에 치중해서 공부하고, 스프링 주간에 가면 할 게 많으니 빠트리지 말고 모조리 챙기고 가도록 하자.