2025. 6. 2. 21:55ㆍJava/Android
저번에 로그인을 구현해봤는데, 회원가입도 다를 게 없었습니다. 오히려 더 간단했습니다.
회원가입 시에는 이메일, 비밀번호, 이름, 나이, 전화번호를 입력받아서 DB에 저장하고, 이메일은 중복확인을 통해 이미 DB에 존재하는 이메일인지 체크할 겁니다.
백엔드 설정
1. 이메일 중복 체크 시 사용할 요청/응답 DTO 추가
이메일 중복 체크 시 사용할 DTO를 추가할 건데, 이름은 EmailCheckRequest, EmailCheckResponse로 하였습니다.
프론트에서 중복 체크를 요청할 때 이메일 값만 넘길 것이므로, username(변수명은 security랑 혼동하지 않기 위해 username으로 하였음)만 정의했습니다.
응답 때는 존재 여부만 전달해주면 되기 때문에 exists Boolean 변수를 정의했습니다.
public class EmailCheckRequest {
public String username;
public String getUsername() {
return username;
}
}
public class EmailCheckResponse {
private boolean exists;
public EmailCheckResponse(boolean exists) {
this.exists = exists;
}
public boolean isExists() {
return exists;
}
}
2. 이메일 중복 체크를 처리할 Controller 메서드 추가
기존에 있던 AuthController를 사용할 것이고, 요청받은 이메일 값을 DB에서 조회하여 존재할 경우 true를, 존재하지 않을 경우 false를 반환합니다. URL 값은 원하시는대로 하셔도 됩니다. ex) validate-email, email-check 등등..
@PostMapping("/check-email")
public ResponseEntity<?> checkEmail(@RequestBody EmailCheckRequest request) {
boolean exists = authService.checkEmail(request.getUsername());
return ResponseEntity.ok(new EmailCheckResponse(exists));
}
3. 이메일 중복 체크를 처리할 Service 메서드 추가
실제로는 SQL의 COUNT를 통해서 존재 여부를 파악할 것이므로 0보다 크면(존재하면) true, 아니면 false를 반환합니다.
public boolean checkEmail(String username) {
return userRepository.countByEmail(username) > 0 ? true : false;
}
4. 실제로 이메일 중복 여부를 판단하는 Repository 메서드 추가
제가 JPA를 사용하는데에도 불구하고 NativeQuery를 자주 사용하는데, 제가 직접 SQL을 짜는거를 더 좋아하기도 하고, 나중에 수정할 때 명명 규칙으로 판단하는게 아니라 쿼리를 직접 보면서 판단할 수 있어서 편해서 애용하고 있습니다.
쿼리는 단순하게 요청받은 이메일을 조건으로 user 테이블에 몇 개가 조회되는지 확인하는 쿼리입니다.
@NativeQuery("SELECT COUNT(*) FROM user WHERE email = ?1")
int countByEmail(String email);
프론트 설정
1. 회원가입 화면 구현
이메일, 비밀번호, 이름, 나이, 전화번호 입력란이 존재하고, 회원가입 버튼, 로그인으로 이동하는 버튼, 메인으로 이동하는 버튼 총 3개가 존재합니다.
사용자가 회원가입 들어갔다가 갇히면 안되니깐 ㅎㅎ 다른 페이지로 나올 수 있도록 버튼을 추가했습니다.
아직은 UI가 단순한데, 기능들이 전부 구현 완료되면 예쁘게.. 그럴듯하게.. 개선해보도록 하겠습니다.
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal">
<!-- 이메일 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<EditText
android:id="@+id/editEmailId"
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="wrap_content"
android:hint="이메일" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\@"
android:textSize="18sp"
android:paddingHorizontal="4dp" />
<Spinner
android:id="@+id/spinnerEmailDomain"
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_check_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="중복확인"
android:layout_marginStart="8dp" />
</LinearLayout>
<!-- 비밀번호 -->
<EditText
android:id="@+id/editPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="비밀번호"
android:inputType="textPassword" />
<!-- 이름 -->
<EditText
android:id="@+id/editName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="이름"
android:inputType="textPersonName"/>
<!-- 나이 -->
<EditText
android:id="@+id/editAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="나이"
android:inputType="number" />
<!-- 전화번호 -->
<EditText
android:id="@+id/editPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="전화번호"
android:inputType="phone"/>
<!-- 버튼들 -->
<Button
android:id="@+id/btnSignup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="회원가입" />
<Button
android:id="@+id/btnGoLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="로그인으로 돌아가기"
android:layout_marginTop="12dp"/>
<Button
android:id="@+id/btnGoMain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="메인으로 이동"
android:layout_marginTop="12dp"/>
</LinearLayout>
</ScrollView>
그리고 이렇게 view를 추가하면 AndroidManifest.xml 파일에 추가로 정의하셔야합니다!! 안 하시면 안드로이드에서 해당 view 페이지를 불러오지를 못해요.
<activity android:name=".activity.RegisterActivity">
</activity>
2. 이메일 중복 체크 요청/응답 DTO 추가
백엔드에서 설정했던 것처럼 요청은 이메일 값만 담을 username만 정의하고, 응답은 exists Boolean 변수를 정의했습니다.
public class EmailCheckRequest {
private String username;
public EmailCheckRequest() {}
public EmailCheckRequest(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
public class EmailCheckResponse {
public boolean exists;
public EmailCheckResponse() {
}
public boolean isExists() {
return exists;
}
public void setExists(boolean exists) {
this.exists = exists;
}
}
3. RetrofitClient 메서드 추가
구조를 보시면 단순하게 서버 통신을 설정하는 메서드인데, 이를 추가한 이유는 추후에 회원가입이나, 메인 페이지, 회원정보 수정 등등.. 또 다른 서버와의 통신 때를 대비하여 편하게 쓰기 위해 전역으로 만들어놓은 메서드입니다.
보시면 통신 간에 요청/응답을 로그로 출력하는 것도 설정하였는데, 필수는 아니고 개발하면서 어디가 문제인가 확인할 때 편해서 추가해놨습니다.
public static ApiService getApiService() {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.build();
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create()) // JSON 데이터를 자동으로 객체 변환
.build();
}
return retrofit.create(ApiService.class);
}
4. RegisterActivity 추가
이메일 중복 체크를 하기 전에, 이메일이 입력되었는지, 이메일 아이템 선택박스를 선택했는지 등등 검사를 한 뒤에 Spring Boot랑 통신해서 처리하도록 구현하였습니다.
그리고 중복 체크를 하면 isCheckEmail 변수를 true로 설정해주는 것을 확인할 수 있는데, 이는 회원가입을 구현할 때 이메일 중복 체크 여부를 판단하기 위해 미리 추가해놓은 변수입니다.
import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.lgh.flipmarketandroid.R;
import com.lgh.flipmarketandroid.config.ApiService;
import com.lgh.flipmarketandroid.config.RetrofitClient;
import com.lgh.flipmarketandroid.dto.user.EmailCheckRequest;
import com.lgh.flipmarketandroid.dto.user.EmailCheckResponse;
import com.lgh.flipmarketandroid.dto.user.RegisterRequest;
import com.lgh.flipmarketandroid.dto.user.RegisterResponse;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class RegisterActivity extends AppCompatActivity {
private String emailId;
private String selectedEmail;
private String pwd;
private String name;
private String age;
private String phoneNum;
private String fullEmailVal = "";
private boolean isCheckEmail = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
EditText emailEditText = findViewById(R.id.editEmailId);
Spinner emailSpinner = findViewById(R.id.spinnerEmailDomain);
Button checkEmailButton = findViewById(R.id.btn_check_email);
EditText pwdEditText = findViewById(R.id.editPassword);
EditText nameEditText = findViewById(R.id.editName);
EditText ageEditText = findViewById(R.id.editAge);
EditText phoneNumEditText = findViewById(R.id.editPhone);
Button registerButton = findViewById(R.id.btnSignup);
Button loginButton = findViewById(R.id.btnGoLogin);
Button mainButton = findViewById(R.id.btnGoMain);
// 이메일 선택박스 초기화
String[] items = { "선택", "naver.com", "daum.net", "gmail.com", "nate.com", "기타(직접입력)" };
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_spinner_item,
items
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
emailSpinner.setAdapter(adapter);
// 서버 통신 요청
ApiService apiService = RetrofitClient.getApiService();
// "중복확인" 버튼 클릭 시 로직
checkEmailButton.setOnClickListener(e1 -> {
emailId = emailEditText.getText().toString().trim();
selectedEmail = emailSpinner.getSelectedItem().toString();
// 이메일아이디 입력란이 비어있을 경우
if (emailId.isEmpty()) {
Toast.makeText(getApplicationContext(), "이메일을 입력해주세요.", Toast.LENGTH_SHORT).show();
return;
}
// 이메일 선택 박스가 "선택"으로 선택되어있을 경우
if (selectedEmail.equals("선택")) {
Toast.makeText(getApplicationContext(), "이메일 양식을 선택해주세요.", Toast.LENGTH_SHORT).show();
return;
}
// 이메일 선택 박스에 "기타(직접입력)"이 선택되어있고, 이메일아이디의 형식이 이메일 정규식에 맞지 않을 경우
if (selectedEmail.equals("기타(직접입력)") && !emailId.matches("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$")) {
Toast.makeText(getApplicationContext(), "이메일 형식이 잘못되었습니다. 다시 입력해주세요.", Toast.LENGTH_SHORT).show();
return;
}
// "기타(직접입력)" 이 선택되어있으면 이메일아이디 입력란으로 이메일 값 할당, 그게 아닐 경우 콤보박스 값까지 받아서 이메일 값 할당
if (selectedEmail.equals("기타(직접입력)")) {
fullEmailVal = emailId;
} else {
fullEmailVal = emailId + "@" + selectedEmail;
}
// 서버 통신 요청
EmailCheckRequest request = new EmailCheckRequest(fullEmailVal);
// 이메일 중복 여부 판단
apiService.checkEmail(request).enqueue(new Callback<EmailCheckResponse>() {
@Override
public void onResponse(@NonNull Call<EmailCheckResponse> call, @NonNull Response<EmailCheckResponse> response) {
Log.d("RegisterActivity", "응답 성공: " + response.body());
if (response.isSuccessful() && response.body() != null) {
if (response.body().exists) {
Toast.makeText(getApplicationContext(), "이미 사용 중인 이메일입니다.", Toast.LENGTH_SHORT).show();
isCheckEmail = false;
} else {
Toast.makeText(getApplicationContext(), "사용 가능한 이메일입니다.", Toast.LENGTH_SHORT).show();
isCheckEmail = true;
}
}
}
@Override
public void onFailure(@NonNull Call<EmailCheckResponse> call, @NonNull Throwable t) {
Log.e("RegisterActivity", "API 요청 실패: ", t);
Toast.makeText(getApplicationContext(), "통신 오류: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
});
// "로그인 화면으로" 버튼 클릭 시 로그인 페이지로 이동 (애니메이션 구현)
loginButton.setOnClickListener(e3 -> {
Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
});
// "메인으로 이동" 버튼 클릭 시 메인 페이지로 이동 (애니메이션 구현)
mainButton.setOnClickListener(e4 -> {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
});
}
}
5. ApiService POST 요청 메서드 추가
이제 서버로 요청할 때의 URL 정의와 파라미터 설정을 하겠습니다.
URL은 백엔드에서 설정했던 URL과 무조건 동일하게 설정을 하고, 파라미터는 만들어놓은 EmailCheckRequest 파일로 해놓았습니다.
@POST("/api/android/check-email")
Call<EmailCheckResponse> checkEmail(@Body EmailCheckRequest request);
'Java > Android' 카테고리의 다른 글
[Android 공부] Spring Boot와 연계를 통한 회원가입 구현(3) (0) | 2025.06.02 |
---|---|
[Android 공부] Spring Boot와 연계를 통한 로그인 구현 (1) (2) | 2025.05.28 |