ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,32 @@
+package webserver.controller;
+
+import webserver.domain.HttpHeader;
+import webserver.domain.HttpRequest;
+import webserver.domain.HttpResponse;
+import webserver.domain.HttpStatusCode;
+import webserver.exceptions.InvalidRequestException;
+
+import java.io.IOException;
+
+import static service.JwpService.getProfilePage;
+import static service.JwpService.isLogin;
+
+public class ListUserController extends AbstractController {
+    @Override
+    public void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
+        try {
+            httpResponse.send(HttpStatusCode.OK, getProfilePage());
+        } catch (IOException e) {
+            httpResponse.send(HttpStatusCode.INTERNAL_SERVER_ERROR, "읽어들이는데 문제가 발생하였습니다");
+        }
+    }
+
+    @Override
+    public void validateGetRequest(HttpRequest request, HttpResponse response) throws InvalidRequestException {
+        if (!isLogin(request.getHeaders().get(HttpHeader.COOKIE))) {
+            response.getHeaders().add(HttpHeader.LOCATION, "/user/login.html");
+            response.send(HttpStatusCode.FOUND);
+            throw new InvalidRequestException("잘못된 로그인 요청");
+        }
+    }
+}

리뷰: login 상태인지 여부를 Request가 알 수 있지 않을까요? 해당 행동을 Request에게 위임해줍시다!
@@ -0,0 +1,46 @@
+package webserver.controller;
+
+import db.DataBase;
+import model.User;
+import webserver.domain.*;
+import webserver.exceptions.InvalidRequestException;
+
+public class LoginController extends AbstractController {
+    private static final String INVALID_REQUEST_MESSAGE = "userId, password가 필요합니다";
+    @Override
+    public void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
+        User findUser = DataBase.findUserById(httpRequest.getParameters().get("userId"));
+        if(findUser == null){
+            sendLoginFailed(httpResponse);
+            return;
+        }
+        if (!findUser.getPassword().equals(httpRequest.getParameters().get("password"))) {
+            sendLoginFailed(httpResponse);
+            return;
+        }
+        sendLoginSucceeded(httpResponse);
+    }
+
+    @Override
+    public void validatePostRequest(HttpRequest request, HttpResponse response) throws InvalidRequestException {
+        HttpParameters httpParameters = request.getParameters();
+        if (httpParameters.contain("userId") && httpParameters.contain("password")) {
+            return;
+        }
+        response.send(HttpStatusCode.BAD_REQUEST, INVALID_REQUEST_MESSAGE);
+        throw new InvalidRequestException(INVALID_REQUEST_MESSAGE);
+    }
+
+    private void sendLoginSucceeded(HttpResponse httpResponse) {
+        httpResponse.getHeaders().add(HttpHeader.SET_COOKIE, "logined=true; Path=/");
+        httpResponse.getHeaders().add(HttpHeader.LOCATION, "/index.html");
+        httpResponse.send(HttpStatusCode.FOUND);
+    }
+
+    private void sendLoginFailed(HttpResponse httpResponse) {
+        httpResponse.getHeaders().add(HttpHeader.SET_COOKIE, "logined=false; Path=/");
+        httpResponse.getHeaders().add(HttpHeader.LOCATION, "/user/login_failed.html");
+        httpResponse.send(HttpStatusCode.FOUND);
+    }
+
+}

리뷰: response header에 cookie, location 셋팅해주는 부분은 Response로 책임을 이동시킬 수 있지 않을까요?
Controller에서 로그인여부와 location만 httpResponse에게 넘기면 알아서 셋팅 되어야할 것 같아요 :)
@@ -0,0 +1,80 @@
+package webserver.domain;
+
+import utils.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+
+public class HttpRequest {
+    private final HttpHeaders headers;
+    private final HttpParameters parameters;
+    private HttpMethod method;
+    private String body;
+    private String path;
+
+    public HttpRequest(InputStream in) throws IOException {
+        headers = new HttpHeaders();
+        parameters = new HttpParameters();
+        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+
+        readFirstLine(reader);
+        readHeaders(reader);
+        if (headers.contain(HttpHeader.CONTENT_LENGTH)) {
+            body = IOUtils.readData(reader, Integer.parseInt(headers.get(HttpHeader.CONTENT_LENGTH)));
+            parameters.parseAndSet(body);
+        }
+    }
+
+    private void readFirstLine(BufferedReader reader) throws IOException {
+        String[] firstLineTokens = reader.readLine().split(" ");
+        String requestUrl = firstLineTokens[1];
+        method = HttpMethod.valueOf(firstLineTokens[0]);
+        parseUrl(requestUrl);
+    }
+
+    private void readHeaders(BufferedReader reader) throws IOException {
+        String line = reader.readLine();
+        while (isValid(line)) {
+            String[] header = line.split(": ");
+            headers.add(header[0], header[1]);
+            line = reader.readLine();
+        }
+    }
+
+    private boolean isValid(String line) {
+        return (line != null) && !"".equals(line);
+    }
+
+    private void parseUrl(String requestUrl) {
+        if (requestUrl.indexOf('?') == -1) {
+            path = requestUrl;
+            return;
+        }
+        String[] urlToken = requestUrl.split("\\?");
+        path = urlToken[0];
+        parameters.parseAndSet(urlToken[1]);
+    }
+
+    public HttpHeaders getHeaders() {
+        return headers;
+    }
+
+    public HttpParameters getParameters() {
+        return parameters;
+    }
+
+    public HttpMethod getMethod() {
+        return method;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public String getPath() {
+        return path;
+    }
+}

리뷰: Request-Line을 파싱할 때 1, 0 과 같은 의미를 찾기 어려운 로직이 존재하는데요!
firstLine은 스펙상 Method SP Request-URI SP HTTP-Version CRLF 으로 정의됩니다.
스펙을 매치 패턴으로 정의하고 그에 맞게 Index 의미를 부여해주는건 어떨까요?
@@ -0,0 +1,21 @@
+package webserver.controller;
+
+import utils.FileIoUtils;
+import webserver.domain.HttpRequest;
+import webserver.domain.HttpResponse;
+import webserver.domain.HttpStatusCode;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+public class DefaultController extends AbstractController {
+    @Override
+    public void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
+        try {
+            httpResponse.send(HttpStatusCode.OK, FileIoUtils.readStaticHttpFile(httpRequest.getPath()));
+        } catch (URISyntaxException | IOException e) {
+            httpResponse.send(HttpStatusCode.NOT_FOUND);
+        }
+    }
+
+}

리뷰: 웹페이지가 잘 뜨는게 아니라서 소스는 잘 받아오는데 왜 그랬나 싶었는데, 버그였네요. 잘 고쳐서 반영할게요!
@@ -0,0 +1,80 @@
+package webserver.domain;
+
+import utils.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+
+public class HttpRequest {
+    private final HttpHeaders headers;
+    private final HttpParameters parameters;
+    private HttpMethod method;
+    private String body;
+    private String path;
+
+    public HttpRequest(InputStream in) throws IOException {
+        headers = new HttpHeaders();
+        parameters = new HttpParameters();
+        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+
+        readFirstLine(reader);
+        readHeaders(reader);
+        if (headers.contain(HttpHeader.CONTENT_LENGTH)) {
+            body = IOUtils.readData(reader, Integer.parseInt(headers.get(HttpHeader.CONTENT_LENGTH)));
+            parameters.parseAndSet(body);
+        }
+    }
+
+    private void readFirstLine(BufferedReader reader) throws IOException {
+        String[] firstLineTokens = reader.readLine().split(" ");
+        String requestUrl = firstLineTokens[1];
+        method = HttpMethod.valueOf(firstLineTokens[0]);
+        parseUrl(requestUrl);
+    }
+
+    private void readHeaders(BufferedReader reader) throws IOException {
+        String line = reader.readLine();
+        while (isValid(line)) {
+            String[] header = line.split(": ");
+            headers.add(header[0], header[1]);
+            line = reader.readLine();
+        }
+    }
+
+    private boolean isValid(String line) {
+        return (line != null) && !"".equals(line);
+    }
+
+    private void parseUrl(String requestUrl) {
+        if (requestUrl.indexOf('?') == -1) {
+            path = requestUrl;
+            return;
+        }
+        String[] urlToken = requestUrl.split("\\?");
+        path = urlToken[0];
+        parameters.parseAndSet(urlToken[1]);
+    }
+
+    public HttpHeaders getHeaders() {
+        return headers;
+    }
+
+    public HttpParameters getParameters() {
+        return parameters;
+    }
+
+    public HttpMethod getMethod() {
+        return method;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public String getPath() {
+        return path;
+    }
+}

리뷰: 매치 패턴 좋네요 아직 반영을 못하였지만 매치 패턴이라는 아이디어 알려주셔서 감사합니다
@@ -0,0 +1,46 @@
+package webserver.controller;
+
+import db.DataBase;
+import model.User;
+import webserver.domain.*;
+import webserver.exceptions.InvalidRequestException;
+
+public class LoginController extends AbstractController {
+    private static final String INVALID_REQUEST_MESSAGE = "userId, password가 필요합니다";
+    @Override
+    public void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
+        User findUser = DataBase.findUserById(httpRequest.getParameters().get("userId"));
+        if(findUser == null){
+            sendLoginFailed(httpResponse);
+            return;
+        }
+        if (!findUser.getPassword().equals(httpRequest.getParameters().get("password"))) {
+            sendLoginFailed(httpResponse);
+            return;
+        }
+        sendLoginSucceeded(httpResponse);
+    }
+
+    @Override
+    public void validatePostRequest(HttpRequest request, HttpResponse response) throws InvalidRequestException {
+        HttpParameters httpParameters = request.getParameters();
+        if (httpParameters.contain("userId") && httpParameters.contain("password")) {
+            return;
+        }
+        response.send(HttpStatusCode.BAD_REQUEST, INVALID_REQUEST_MESSAGE);
+        throw new InvalidRequestException(INVALID_REQUEST_MESSAGE);
+    }
+
+    private void sendLoginSucceeded(HttpResponse httpResponse) {
+        httpResponse.getHeaders().add(HttpHeader.SET_COOKIE, "logined=true; Path=/");
+        httpResponse.getHeaders().add(HttpHeader.LOCATION, "/index.html");
+        httpResponse.send(HttpStatusCode.FOUND);
+    }
+
+    private void sendLoginFailed(HttpResponse httpResponse) {
+        httpResponse.getHeaders().add(HttpHeader.SET_COOKIE, "logined=false; Path=/");
+        httpResponse.getHeaders().add(HttpHeader.LOCATION, "/user/login_failed.html");
+        httpResponse.send(HttpStatusCode.FOUND);
+    }
+
+}

리뷰: Response에 넘겨주려고 하였지만 Response가 순수하게 HTTP 스펙에 따른 전송만 되도록 간결하게 짜여져있어서 중간 클래스를 만드는 방법으로 고민해보고 있어요.
@@ -0,0 +1,32 @@
+package webserver.controller;
+
+import webserver.domain.HttpHeader;
+import webserver.domain.HttpRequest;
+import webserver.domain.HttpResponse;
+import webserver.domain.HttpStatusCode;
+import webserver.exceptions.InvalidRequestException;
+
+import java.io.IOException;
+
+import static service.JwpService.getProfilePage;
+import static service.JwpService.isLogin;
+
+public class ListUserController extends AbstractController {
+    @Override
+    public void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
+        try {
+            httpResponse.send(HttpStatusCode.OK, getProfilePage());
+        } catch (IOException e) {
+            httpResponse.send(HttpStatusCode.INTERNAL_SERVER_ERROR, "읽어들이는데 문제가 발생하였습니다");
+        }
+    }
+
+    @Override
+    public void validateGetRequest(HttpRequest request, HttpResponse response) throws InvalidRequestException {
+        if (!isLogin(request.getHeaders().get(HttpHeader.COOKIE))) {
+            response.getHeaders().add(HttpHeader.LOCATION, "/user/login.html");
+            response.send(HttpStatusCode.FOUND);
+            throw new InvalidRequestException("잘못된 로그인 요청");
+        }
+    }
+}

리뷰: Request 에 위임하면 파라미터를 매핑하고, 값을 할당하는 Request 역할까지만 맡긴 상태라 중간 클래스를 만드는 방식으로 고민을 해보았어요. 그러다가, Spring 에 있는 ArgumentResolver 와 유사하게, 검증이 필요한 때에만 오버라이딩 하도록 AbstractController에 validate...Request 라는 메서드들을 만들어 보았습니다.

이 경우, 로그인이 필요한 유사한 컨트롤러에 대해서 중복 코드가 생성된다는 부담이 있는데, 로그인 서비스를 통합하는 추상 객체인 로그인 컨트롤러를 만들거나, 나중에 MVC 모델인 어노테이션을 적용할까하는데 이런 방식은 어떤가요?
@@ -0,0 +1,45 @@
+package controller;
+
+import controller.handler.Handler;
+import exception.utils.NoFileException;
+import model.request.HttpRequest;
+import model.response.HttpResponse;
+import model.PathInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public abstract class Controller {
+    private static final Logger log = LoggerFactory.getLogger( Controller.class );
+    protected final Map<PathInfo, Handler> handlers = new HashMap<>();
+    protected String basePath = "";
+
+    public void putHandler(String path, String method, Handler handler) {
+        handlers.put(new PathInfo(basePath + path, method), handler);
+    }
+
+    public void setBasePath(String basePath) {
+        this.basePath = basePath;
+    }
+
+    public boolean hasSameBasePath(String path) {
+        return path.startsWith(basePath);
+    }
+
+    public boolean handle(HttpRequest request, HttpResponse response) throws NoFileException, IOException {
+        for (Map.Entry<PathInfo, Handler> entry : handlers.entrySet()) {
+            log.info("matching {} with controller {}", request.getPath(), entry.getKey().getPath());
+
+            if (request.getPath().matches(entry.getKey().getPath())) {
+                log.info("handling {} with controller {}", request.getPath(), entry.getKey().getPath());
+                entry.getValue().handle(request, response);
+                return true;
+            }
+        }
+        return false;
+    }
+}

리뷰: handlers 는 굳이 LinkedHashMap 을 사용한 이유가 있을까요?
@@ -0,0 +1,30 @@
+package model;
+
+import java.util.Objects;
+
+public class PathInfo {
+    private final String path;
+    private final String method;
+
+    public PathInfo(String path, String method) {
+        this.path = path;
+        this.method = method;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PathInfo that = (PathInfo) o;
+        return Objects.equals(path, that.path) && Objects.equals(method, that.method);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(path, method);
+    }
+
+    public String getPath() {
+        return path;
+    }
+}

리뷰: PathInfo 객체를 별도로 선언하셨군요 💯
@@ -0,0 +1,44 @@
+package controller;
+
+import controller.handler.SecuredHandler;
+import model.request.HttpRequest;
+import model.response.HttpResponse;
+import service.UserService;
+import view.View;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class UserController extends Controller {
+    {
+        setBasePath("/user");
+        putHandler("/create", "POST", this::handleCreate);
+        putHandler("/login", "POST", this::handleLogin);
+        putHandler("/list", "GET", new SecuredHandler(this::handleUserList));
+        putHandler("/logout", "GET", new SecuredHandler(this::handleLogout));
+    }
+
+    private UserService userService = new UserService();
+
+    public void handleCreate(HttpRequest request, HttpResponse response) {
+        userService.addUser(request.getParsedBody());
+        response.sendRedirect("/index.html");
+    }
+
+    public void handleLogin(HttpRequest request, HttpResponse response) {
+        if (!userService.login(request.getParsedBody())) {
+            response.setCookie("logined=false").sendRedirect("/user/login_failed.html");
+            return;
+        }
+        response.setCookie("logined=true").sendRedirect("/index.html");
+    }
+
+    public void handleUserList(HttpRequest request, HttpResponse response) throws IOException {
+        byte[] body = View.getUsersView(userService.findAll(), "/user/list");
+        response.sendView(body);
+    }
+
+    public void handleLogout(HttpRequest request, HttpResponse response) throws IOException {
+        response.setCookie("logined=false").sendRedirect("/index.html");
+    }
+}

리뷰: DataBase 에 직접 접근하지 말고 별도의 서비스 클래스로 분리해보면 어떨까요?
@@ -0,0 +1,44 @@
+package controller;
+
+import controller.handler.SecuredHandler;
+import model.request.HttpRequest;
+import model.response.HttpResponse;
+import service.UserService;
+import view.View;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class UserController extends Controller {
+    {
+        setBasePath("/user");
+        putHandler("/create", "POST", this::handleCreate);
+        putHandler("/login", "POST", this::handleLogin);
+        putHandler("/list", "GET", new SecuredHandler(this::handleUserList));
+        putHandler("/logout", "GET", new SecuredHandler(this::handleLogout));
+    }
+
+    private UserService userService = new UserService();
+
+    public void handleCreate(HttpRequest request, HttpResponse response) {
+        userService.addUser(request.getParsedBody());
+        response.sendRedirect("/index.html");
+    }
+
+    public void handleLogin(HttpRequest request, HttpResponse response) {
+        if (!userService.login(request.getParsedBody())) {
+            response.setCookie("logined=false").sendRedirect("/user/login_failed.html");
+            return;
+        }
+        response.setCookie("logined=true").sendRedirect("/index.html");
+    }
+
+    public void handleUserList(HttpRequest request, HttpResponse response) throws IOException {
+        byte[] body = View.getUsersView(userService.findAll(), "/user/list");
+        response.sendView(body);
+    }
+
+    public void handleLogout(HttpRequest request, HttpResponse response) throws IOException {
+        response.setCookie("logined=false").sendRedirect("/index.html");
+    }
+}

리뷰: 위와 마찬가지로 직접 DataBase 에 접근하기 보다 서비스 객체를 사용하는 방법은 어떨까요? 
넘겨준 값을 기반으로 로그인 되었는지 되지 않았는지 여부를 boolean 값으로 조회할 수 있다면
좀 더 간결한 코드로 개선할 수 있을것 같습니다.
@@ -0,0 +1,45 @@
+package controller;
+
+import controller.handler.Handler;
+import exception.utils.NoFileException;
+import model.request.HttpRequest;
+import model.response.HttpResponse;
+import model.PathInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public abstract class Controller {
+    private static final Logger log = LoggerFactory.getLogger( Controller.class );
+    protected final Map<PathInfo, Handler> handlers = new HashMap<>();
+    protected String basePath = "";
+
+    public void putHandler(String path, String method, Handler handler) {
+        handlers.put(new PathInfo(basePath + path, method), handler);
+    }
+
+    public void setBasePath(String basePath) {
+        this.basePath = basePath;
+    }
+
+    public boolean hasSameBasePath(String path) {
+        return path.startsWith(basePath);
+    }
+
+    public boolean handle(HttpRequest request, HttpResponse response) throws NoFileException, IOException {
+        for (Map.Entry<PathInfo, Handler> entry : handlers.entrySet()) {
+            log.info("matching {} with controller {}", request.getPath(), entry.getKey().getPath());
+
+            if (request.getPath().matches(entry.getKey().getPath())) {
+                log.info("handling {} with controller {}", request.getPath(), entry.getKey().getPath());
+                entry.getValue().handle(request, response);
+                return true;
+            }
+        }
+        return false;
+    }
+}

리뷰: handle 메서드의 첫번째 인자는 HttpRequest 인데, 두번째 인자는 OutputStream 이네요. 
HttpRequest / HttpResponse 형태가 더 적합해 보입니다 😄
@@ -0,0 +1,66 @@
+package controller;
+
+import controller.error.ErrorHandlerFactory;
+import controller.handler.Handler;
+import exception.utils.NoFileException;
+import model.request.HttpRequest;
+import model.response.HttpResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import webserver.RequestHandler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ControllerManager {
+    private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
+    private static final List<Controller> controllers = new ArrayList<>();
+    static {
+        registerController(new ViewController());
+        registerController(new UserController());
+    }
+
+    public static void registerController(Controller controller) {
+        controllers.add(controller);
+    }
+
+    public static void runControllers(Socket connection) {
+        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
+            HttpRequest httpRequest = HttpRequest.of(in);
+            HttpResponse httpResponse = HttpResponse.of(out);
+            handleRequest(httpRequest, httpResponse);
+        } catch (IOException e) {
+            log.error("{} {}", e.getMessage(), e.getStackTrace());
+        }
+    }
+
+    private static void handleRequest(HttpRequest httpRequest, HttpResponse httpResponse) {
+        List<Controller> selectedControllers = controllers.stream()
+                .filter(controller -> controller.hasSameBasePath(httpRequest.getPath()))
+                .collect(Collectors.toList());
+        try {
+            for (Controller controller : selectedControllers) {
+                if (controller.handle(httpRequest, httpResponse)) return;
+            }
+
+            defaultHandle(httpRequest, httpResponse);
+        } catch (NoFileException | IOException | RuntimeException e) {
+            Handler errorHandler = ErrorHandlerFactory.getHandler(e);
+            try {
+                errorHandler.handle(httpRequest, httpResponse);
+                log.warn(e.getMessage());
+            } catch (Exception handleError) {
+                log.error("{} {}", handleError.getMessage(), handleError.getStackTrace());
+            }
+        }
+    }
+
+    public static void defaultHandle(HttpRequest request, HttpResponse response) throws NoFileException {
+        response.forward("./templates", request.getPath());
+    }
+}

리뷰: 컨트롤러 매니저까지 작성하셨네요 👍
@@ -1,16 +1,48 @@
 package model;
 
+import exception.user.WrongEmailException;
+import exception.user.WrongIdException;
+import exception.user.WrongNameException;
+import exception.user.WrongPasswordException;
+
 public class User {
-    private String userId;
-    private String password;
-    private String name;
-    private String email;
+    private final String userId;
+    private final String password;
+    private final String name;
+    private final String email;
 
     public User(String userId, String password, String name, String email) {
-        this.userId = userId;
-        this.password = password;
-        this.name = name;
-        this.email = email;
+        this.userId = validateId(userId);
+        this.password = validatePw(password);
+        this.name = validateName(name);
+        this.email = validateEmail(email.replace("%40", "@"));
+    }
+
+    private String validatePw(String password) {
+        if(!password.matches("^[\\w]{1,30}$")){
+            throw new WrongPasswordException();
+        }
+        return password;
+    }
+
+    private String validateId(String userId) {
+        if(!userId.matches("^[\\w]{1,30}$"))
+            throw new WrongIdException();
+        return userId;
+    }
+
+    private String validateName(String name) {
+        if (!name.matches("^[\\w가-힣]{1,30}$")) {
+            throw new WrongNameException();
+        }
+        return name;
+    }
+
+    private String validateEmail(String email) {
+        if(!email.matches("^[\\w]{1,30}@[\\w]{1,30}.[\\w]{1,30}$")){
+            throw new WrongEmailException();
+        }
+        return email;
     }
 
     public String getUserId() {

리뷰: validation 로직을 추가하신 것은 긍정적입니다.
다만 User 객체가 인스턴스화 될 때마다 validate 메서드가 호출되게 되는데요. 
User 라는 도메인 모델이 갖추고 있어야할 필수적인 validation 인지
아니면 새로운 User 를 생성하는 사용자의 요청에서만 필요한 validation 인지에 따라서
User 객체에서 처리하는게 적합할 수도 있고 / Controller 영역에서 처리하는 것이 더 나을 수도 있습니다.
이부분은 한번 고민해봐주세요 😄