업무상 필요해서 급하게 공부하게 된 모드버스 프로토콜과 RS485통신 개념인데, 확실히 개념을 알고 나니 그동안 다른 담당자가 정리해준 내용에 대해 정확하게 이해하게 됐다. 담당자의 퇴사로 인해 직접 RS485를 구현해야하는 상황이 돼서 걱정이 많았는데, 공부할 시간을 가지고 천천히 접근하니 할수있겠는데 하는 생각이 들기도 했다. 간단한 내용이지만, 업무에 도움이 되는 기본적인 내용이라 블로그에 정리해봤다. 화이팅..!
앱 자체를 현업에서 개발한것도 처음이고, 블투와 연결했을때 이런 오류가 발생한다는 사실도 처음이라 당황했었다. 구글링했을때도 관련 자료가 많이 없어서 문제해결에도 많은 시간이 소요됐지만, 해결해내서 넘 뿌듯했다 ^_^ 찾아보니 예전에 어떤 앱에서는 에어팟 연결하면 앱이 리로드 되는 이슈가 있었다고 하던데, 이런 오류가 지속해서 발생했다면 사용성이 정말 최악이었을것 같다. 웹뷰를 만들때의 설정이 잘못되어서 그럴수있다는 조언도 들었는데, 아직 내 수준으로는 어느 부분에서 설정이 잘못되었는지를 찾기가 어려웠다. 이럴때일수록 간절한건 시니어님의 존재지만,, 어떻게든 혼자서 해결해보는것도 참 좋은 경험인 것 같다.. 더이상 이 오류는 무섭지 않으니까!
초반에 USB를 생성하는 과정과, 우분투 설치 후 재부팅 되는 시간이 생각보다 길었다. 또 크게 어려운 것 없이 화면이 시키는대로 작업하면 큰 문제없이 설치할 수 있었다. 개인적으로는 매번 이런저런 설정을 기억했다가 똑같이 해야하는게 번거로워서 모든 선택지를 이미지로 남겨놓는 것을 선호하는데, 그 이미지를 활용해 기록해두면 추후 동일한 작업을 할때 뭔가 기계적으로? 따라할 수 있어서 좋은 것 같다ㅎㅎ 그리고 미니pc의 경우 F2, Esc 등 부팅모드로 진입하는 키가 서로 다르기때문에 본인이 설치할 pc의 부팅모드 진입키는 사전에 알아두는 것이 좋은 것 같다. 제품에 따라서 특정 키를 연타해야 접근이 가능하다고 하던데, ASRock DeskMini B660 제품의 경우 F11 버튼을 한번만 눌러도 바로 접근이 가능해서 편리했다!
항상 시간에 쫓기는 개발을 할수밖에 없었던 업무 특성상 기능을 붙이는 게 더 중요했기 때문에 홈페이지의 favicon 설정은 뒷전이었다. 하지만 미팅때 실제 유저들을 만나면 해당 웹페이지에 쉽게 접근하고, 구분하기 위해 아이콘을 넣어달라는 요청이 많았다. 간단한 작업이었지만 우선순위가 긴급한 안건들을 처리하고 이제야 시간이 나 프로젝트에 favicon 설정을 시도했고, 간단한 설정이었지만 프로젝트 구조에 따라 해당 파일을 넣어야하는 위치가 달라지기 때문에 시행착오를 겪었다. 구글링을 해도 spring 프로젝트에서 어디에 favicon 파일을 넣고 설정해야하는지 알려주는 글이 없어서 아쉬워서 남겨보는 글이니 누군가에게는 도움이 되길 바란다.
프로젝트 폴더구조
어떤 환경설정을 하는지에 따라 달라질 수 있지만, 우리는 src/main/webapp 하위에 resources 폴더가 있고 css, js, font, img 등 웹페이지에서 사용하는 코드들을 폴더별로 분리하여 관리한다. tiles도 사용중이기 때문에 웹페이지의 기본 틀이 되는 classic.jsp 파일의 head 태그 안에 link 태그를 넣어 favicon 설정을 한 뒤 src/main/webapp/resources 폴더 바로 아래에 favicon.ico 파일을 추가하여 설정을 완료했다.
사수님이 퇴사하고, 주로 비즈니스 로직을 다루는 백엔드 작업을 할 일이 많아졌다. 기존에는 코드를 마치 레시피처럼 외워서 개발해왔기 때문에 문제가 생겼을 때 스스로 디버깅하기가 어려웠다. 그래서 주 1회 퇴근 후 스터디를 통해 스프링 입문부터 공부하기로 결심했다. 오늘은 그 첫 번째 챕터인 웹개발 기초에 대해 알아보고자 한다.
스프링을 통한 웹 개발 방법
스프링으로 웹개발을 할 때 아래 세 가지 방법을 사용할 수 있다.
1. 정적컨텐츠 : HTML 파일 그대로 웹 브라우저에 전달
2. MVC와 템플릿 엔진 : 서버에서 HTML 파일을 동적으로 변환한 뒤 웹 브라우저에 전달
3. API : JSON으로 데이터를 클라이언트에 직접적으로 전달
각 방법의 특징과 스프링에서의 동작방식에 대해서 알아보자.
정적컨텐츠의 동작과 특징
정적컨텐츠는 클래스 경로나 ServletContext 루트에 있는 /static(or /public, /resources, META-INF/resources) 디렉터리에서 제공한다. 해당 폴더 내에 HTML 파일을 추가하는 경우 이를 그대로 반환한다는 특징이 있다. 별도의 설정 없이 스프링부트에서 기본적으로 제공하는 기능이지만 Spring MVC의 ResourceHttpRequestHandler를 사용하여 동작하기 때문에 자체 WebMvcConfigurer를 추가하고 addResourceHandlers 메소드를 재정의하여 해당 동작을 수정할 수도 있다.(하지만 이렇게 수정해서 사용할 이유가 없음..) 또한, 기본적으로 리소스는 /**에 mapping되며, 이 경로를 수정하고 싶은 경우 spring.mvc.static-path-pattern 속성을 변경하여 mapping 경로를 수정하는 것도 가능하다.
스프링부트로 실행한 웹 브라우저에서 정적컨텐츠인 HTML파일을 표시하기 위해 톰캣 서버로 요청을 보내면, 스프링부트의 스프링컨테이너에서는 해당 url과 mapping 된 controller를 찾는다. 하지만 controller가 없는 정적컨텐츠이기 때문에, static 폴더 등을 살펴보고 동일한 이름의 HTML파일을 브라우저로 전달하는 방식으로 동작한다.
MVC와 템플릿 엔진의 동작과 특징
이 방법의 특징은 한번 렌더링 한 HTML을 브라우저에 전달한다는 점이다. 이해를 위해 MVC란 무엇인지에 대해 먼저 알아보자. MVC는 Model, View, Controller의 구성을 뜻하며, 역할에 따라 기능을 분리하여 제공하기 위해 사용한다. Model은 controller가 화면에 표시해야 하는 것들을 정리해서 담는 곳이며, 이를 view에 전달하는 역할을 한다. View는 오로지 화면을 그리는 일만을 담당하며, Controller는 비즈니스 로직을 담당하며, 요청을 받았을 때 화면 뒷단에서 처리해야 하는 일들을 담는 역할이다. 여기서 View가 php, jsp 등 템플릿엔진을 통해 Model, Controller의 정보를 받아 HTML을 다시 렌더링 하고 그 결과를 브라우저에 전달하도록 개발하면 된다. 이처럼 템플릿엔진을 MVC방식으로 쪼개서 동작하도록 만들었다면 이 방법을 사용한 것이라고 이야기할 수 있다.
웹 브라우저의 요청을 톰캣서버로 보냈을 때 스프링컨테이너에서 가장 먼저 확인하는 Controller단에서 mapping된 정보가 존재한다면, 컨트롤러의 동작을 먼저 수행하고 이를 바탕으로 viewResolver가 HTML파일을 렌더링해 변환을 마친 HTML을 웹 브라우저로 전달하는 방식으로 동작한다.
API의 동작과 특징
API 방식은 데이터를 바로 내려준다는 특징을 가진다. 이 방식을 사용하기 위해서는 Controller에서 @ResponseBody라는 어노테이션을 사용해야 하는데, 이는 해당 데이터를 HTML의 <Body> 태그 내부로 전달한다는 뜻이 아닌, HTTP 내 응답 Body부분에 해당 데이터를 직접 넘겨주겠다고 정하는 것을 뜻한다. 따라서 return 값이 view 템플릿을 거치는 것이 아니라 데이터 형식 그대로 전달된다는 특징이 있다.
웹 브라우저를 통해 톰캣서버로 전달된 요청은 MVC 때와 동일하게 스프링컨테이너 내 mapping된 Controller로 전달되고, 해당 Controller에 적용된 @ResponseBody가 파라미터를 통해 생성한 객체를 그대로 return으로 반환한다. 이때 HttpMessageConverter가 JsonConverter를 통해 객체를 JSON 형태로 변환하여 이를 웹 브라우저에 전달하는 방식으로 동작한다.
@Controller
public class HelloController {
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
정리하며,
이렇게 오늘은 스프링을 통해 웹개발하기 위한 방법 세 가지에 대해 공부했다. 부끄럽지만 평소 내가 사용하고 있는 방식이 API인지도 정확히 모르고 사용해 왔었고, view 구성을 위해 model에 데이터를 넘겨주는 방법도 이제야 제대로 알게 된 것 같다. 아직 기본적인 내용 밖에 담지 못했지만, 점차 학습하며 각 방법을 적절하게 활용하는 개발자가 되어야겠다💪
매일 아침에 출근해 업무를 시작하기 전, 5-10분 동안 깃헙의 잔디를 채울 겸 JS공부를 할 겸 제주코딩베이스캠프의 JS 100제 강의에서 제공되는 문제를 푸는 중이다. 사실 아침에 해설 영상까지 볼 시간은 없어서 문제를 푼 뒤에 답안을 보고 내가 맞게 풀었는지 정도만 확인하고 있는데, 오늘 풀었던 13번 문제를 푸는 과정에서 문득 이런 의문점을 갖게 됐다.
const a = ['수성', '금성', '지구', '화성', '목성', '토성', '천왕성', '해왕성'];
let num = prompt('숫자를 입력하세요');
console.log(a[num-1]);
사용자로부터 입력값을 받아 해당 순서의 행성 이름을 출력하는 문제였는데, 위 코드가 이번 문제의 답안이었다.
간단한 문제였고, 무난하게 풀 수 있었는데 내가 생각한 답안은 책에서 제공하는 답안과는 조금 달랐다.
const a = ['수성', '금성', '지구', '화성', '목성', '토성', '천왕성', '해왕성'];
let num = prompt('숫자를 입력하세요');
num -= 1;
console.log(a[num]);
위 답안이 내가 생각한 방법인데, 책에서 제공하는 답안보다 코드 한 줄이 더 많은 점이 제법 거슬렸다.
이런 코드를 작성하게 된 이유는 콘솔로 값을 찍다가 console.log(a[num+1]) 을 우연히 실행시켜봤는데 값이 undefined가 나오는 게 아닌가. 그래서 당연히 console.log(a[num-1]) 도 undefined가 나오지 않을까 해서 저렇게 num 계산을 별도로 해주는 답안을 작성하게 됐었다. 사실 책에서 제공하는 답안을 보고 의문이 들었는데, 왜 num+1했을 때는 출력이 안되고 num-1을 했을때는 값 출력이 정상적으로 되는지를 도저히 이해할 수가 없었다. 그래서 구글링도 해봤는데 도저히 어떤 키워드로 검색해야 할지 감이 잡히지 않아 같이 공부하는 동료들에게 질문을 던졌다.
9시 5분쯤부터 문제를 풀기 시작했는데, 질문한 시간은 24분ㅋㅋㅋㅋ
이렇게 질문을 하고 업무를 보고 있었는데 동료들이 다양한 답을 던져주기 시작했다.
그중 타입 때문에 이런 현상이 발생하는 게 아니냐는 이야기가 있었는데, 정말 유레카스러웠던 순간이었다.
동료들이 해당 값의 타입을 찍어 봤을 때 동작이 이상한 것 같다고 의견을 주셔서 확인해보니 정말 이상한 타입이 나왔고, 스스로 문제를 살필 때는 도저히 생각하지 못했던 부분이었던지라해결의 실마리를 찾은 기분이었다. 사실 JS에서는 타입을 별도로 선언해주지 않고 사용하기 때문에 암묵적 형변환에 의존해서 사용하게 되는데, 포스팅을 하려고 찾아보니 이러한 기능은 가급적이면 사용하지 않는게 좋다고 한다. 내가 겪은 의문점도 형변환 때문에 생겼다고 생각하니 사람들이 왜 가급적 쓰지 말라고 했었는지 체감할 수 있었다.
더 정확하게 이야기하자면, prompt를 통해 받아온 값은 String 형태로 저장되고 있었다.
만약 prompt로 값:1을 입력받아왔다면 a[num]은 숫자 1로 암묵적 형변환이 되어 a[1]의 값인 '금성'이 출력되고,
a[num-1]도 -1을 처리하기 위해 동일한 암묵적 형변환이 발생해 a[0]에 해당하는 값인 '수성'을 출력했던 것이다.
그렇다면 왜 a[num+1]만 undefined가 나온 것일까?
num+1에는 암묵적 형변환이 일어나지 않아서 해당 출력 값이 표시된 것일까?
그건 아니다. a[num+1]인 경우 JS엔진이 이를 a['1'+1] 로 인식해 a[2]가 아닌 a['11']로 처리했고 이후에 형변환이 일어나 a[11]을 출력하려고 보니 배열에 인덱스가 11인 값이 없기 때문에 undefined가 출력된 것이다. 이렇게 보니 간단한 이유인데 이 생각을 못해서 한참 문제와 씨름했던걸 떠올리니 다시 한번 사람들이 왜 타입스크립트를 쓰는지 알 것 같기도 했다🤦♀️
이러한 문제를 해결하기 위해서는 명시적인 형변환을 사용해주면 된다. prompt를 통해 받아온 입력 값이 String 타입이지만, 나는 해당 값을 Number로 사용할 예정이기 때문에 prompt('숫자를 입력하세요')라는 function을 Number()로 감싸 명시적인 형변환을 해주면 된다. 이렇게 명시적으로 형변환을 시켜준 뒤에는 console.log(a[num+1])을 찍어도 내가 의도한 '지구'라는 출력 값을 표시하게 된다.
그래서 감동받은 점이 어떤 부분이길래 이렇게 서론을 길게 설명하느냐 하면, 위에서 보인 내 태도와 관한 피드백이었다. 나는 평소에 내 주제, 다르게 말하자면 객관화가 잘 되는 편이라고 생각했다. 그래서 내가 할 수 있는 일과 하지 못하는 일을 명확하게 구분하는 편이었고, 이렇게 일하는 방식이 개선점이 되리라고 생각해본 적이 없었던 것 같다. 하지만 단순히 일이 아닌 배움이나 성장의 시각에서 본다면 이러한 나의 모습이 소극적인 자세로 비춰질 수도 있겠다는 생각이 들었다.
사실 예전의 나는 불가능해보이는 일에 도전하는 것을 꺼려했었다. 어짜피 안될거 해봤자 뭐해 라고 생각했을지도 모른다. 그런 내가 조금 더 나이를 먹어가면서, 도전을 피해서 얻지 못한 결과들에 대해 후회하는 경험을 하게 됐다. 항상 과거에 놓친 기회들만 후회하다보니 그 시간이 너무 아깝다고 느껴졌고, 그 때 도전했다면, 그 때 시도했다면 뭔가 다른 결과를 만들 수 있지 않았을까하는 생각에 다다르게 됐다. 이를 통해 도전은 소중하고 적어도 후회를 남기지는 않는다는 배움을 얻었고 지금은 나름 도전을 즐기는 사람이 되었다고 생각했는데, 아직도 내게 그런 습관이 남아있었나보다.
참.. 의식하면서 살아가지 않으면 쉽게 놓칠 수 있는 부분인 것 같다. 그래도 이번 피드백을 통해 새로운 시각으로 나에 대해 되돌아 볼 수 있었던 것 같아서 너무 좋은 기회였다. 익명으로 전달받은 내용이라 직접 감사를 표할 수는 없지만, 그래도 이번 과정을 통해 만나게 된 모든 동료들과 인연에게 감사하는 마음을 전하고 싶다 (´▽`ʃ♡ƪ)
아래는 해당 피드백의 내용 전문이다. 넘 따듯하고 진심으로 조언해주시는 모습에 감동의 눈물바다..🌊😭